summaryrefslogtreecommitdiff
path: root/js/videojs/build
diff options
context:
space:
mode:
Diffstat (limited to 'js/videojs/build')
-rw-r--r--js/videojs/build/assets.js48
-rw-r--r--js/videojs/build/current-changelog.js35
-rw-r--r--js/videojs/build/docs-only.js19
-rw-r--r--js/videojs/build/docs/styles/videojs.css66
-rw-r--r--js/videojs/build/fix-api-docs.js43
-rw-r--r--js/videojs/build/jsdoc-typeof-plugin.js12
-rw-r--r--js/videojs/build/jsdoc-workarounds.js13
-rw-r--r--js/videojs/build/license-header.txt11
-rw-r--r--js/videojs/build/minify.js48
-rw-r--r--js/videojs/build/netlify-docs.js23
-rw-r--r--js/videojs/build/netlify.js38
-rw-r--r--js/videojs/build/readme-version.js16
-rw-r--r--js/videojs/build/rollup-exclude-lines.js41
-rw-r--r--js/videojs/build/sandbox.js31
-rw-r--r--js/videojs/build/test-a11y.js27
-rw-r--r--js/videojs/build/translations.js54
16 files changed, 525 insertions, 0 deletions
diff --git a/js/videojs/build/assets.js b/js/videojs/build/assets.js
new file mode 100644
index 0000000..738b4d2
--- /dev/null
+++ b/js/videojs/build/assets.js
@@ -0,0 +1,48 @@
+/* eslint-disable no-console */
+const fs = require('fs');
+const zlib = require('zlib');
+const filesize = require('filesize');
+const Table = require('cli-table');
+const path = require('path');
+const sh = require('shelljs');
+
+// find all js/css files in the dist dir
+// but ignore any files in lang, example, or font directories
+const filepaths = sh
+ .find(path.join(__dirname, '..', 'dist', '**', '*.{js,css}'))
+ .filter((filepath) => !(/\/(lang|example|font)\//).test(filepath));
+
+// map all files that we found into an array of
+// table entries the filepath, file size, and gzip size.
+Promise.all(filepaths.map(function(filepath) {
+ return new Promise(function(resolve, reject) {
+ const readStream = fs.createReadStream(filepath);
+ const writeStream = fs.createWriteStream(filepath + '.gz');
+ const gzip = zlib.createGzip();
+
+ readStream.pipe(gzip).pipe(writeStream).on('close', function() {
+ const gzStat = fs.statSync(filepath + '.gz');
+ const fileStat = fs.statSync(filepath);
+
+ fs.unlinkSync(filepath + '.gz');
+
+ resolve([filepath.split('dist/')[1], filesize(fileStat.size), filesize(gzStat.size)]);
+ })
+ .on('error', reject);
+ });
+})).then(function(lines) {
+ // log all the files and there sizes using a cli table
+ const table = new Table({
+ head: ['filename', 'size', 'gzipped'],
+ colAligns: ['left', 'right', 'right'],
+ style: {
+ border: ['white']
+ }
+ });
+
+ table.push.apply(table, lines);
+ console.log(table.toString());
+
+}).catch(function(err) {
+ console.error(err.stack);
+});
diff --git a/js/videojs/build/current-changelog.js b/js/videojs/build/current-changelog.js
new file mode 100644
index 0000000..a80ef88
--- /dev/null
+++ b/js/videojs/build/current-changelog.js
@@ -0,0 +1,35 @@
+/* eslint-disable no-console */
+
+const unified = require('unified');
+const markdown = require('remark-parse');
+const stringify = require('remark-stringify');
+const fs = require('fs');
+
+module.exports = function() {
+ const processor = unified()
+ .use(markdown, {commonmark: true})
+ .use(stringify);
+
+ const ast = processor.parse(fs.readFileSync('./CHANGELOG.md'));
+ const changelog = [];
+
+ changelog.push(processor.stringify(ast.children[0]));
+
+ // start at 1 so we get the first anchor tag
+ // and can break on the second
+ for (let i = 1; i < ast.children.length; i++) {
+ let item = processor.stringify(ast.children[i]);
+
+ if (/^<a name="/.test(item)) {
+ break;
+ }
+
+ if (/^###/.test(item)) {
+ item = '\n' + item + '\n';
+ }
+
+ changelog.push(item);
+ }
+
+ return changelog.join('\n');
+};
diff --git a/js/videojs/build/docs-only.js b/js/videojs/build/docs-only.js
new file mode 100644
index 0000000..c8abcb9
--- /dev/null
+++ b/js/videojs/build/docs-only.js
@@ -0,0 +1,19 @@
+const sh = require('shelljs');
+const path = require('path');
+
+module.exports = function(commit, commitRange) {
+ const SINGLE_COMMIT = `git diff-tree --no-commit-id --name-only -r ${commit}`;
+ const COMMIT_RANGE = `git diff --name-only ${commitRange}`;
+
+ let command = SINGLE_COMMIT;
+
+ if (commitRange) {
+ command = COMMIT_RANGE;
+ }
+
+ const output = sh.exec(command, {async: false, silent: true}).stdout;
+
+ const files = output.split('\n').filter(Boolean);
+
+ return files.every((file) => file.startsWith('docs') || path.extname(file) === '.md');
+};
diff --git a/js/videojs/build/docs/styles/videojs.css b/js/videojs/build/docs/styles/videojs.css
new file mode 100644
index 0000000..1e5447d
--- /dev/null
+++ b/js/videojs/build/docs/styles/videojs.css
@@ -0,0 +1,66 @@
+#resizer,
+footer {
+ background-color: #ECEEF1;
+ color: #868688;
+ padding: 3px 10px;
+}
+
+.footer-text {
+ padding: 3px;
+ display: block;
+}
+
+footer .copyright {
+ float: left;
+}
+
+footer .logo {
+ display: none;
+}
+
+.sidebar-title {
+ background: center / contain no-repeat url(https://videojs.com/logo-white.png);
+ text-indent: -999em;
+}
+
+.light .sidebar-title,
+.dark .link-vjs a:before,
+.dark .link-gh a:before,
+.dark .link-tw a:before {
+ -webkit-filter: invert(80%);
+ filter: invert(80%);
+}
+
+.link-vjs a:before,
+.link-gh a:before,
+.link-tw a:before {
+ content: "";
+ display: inline-block;
+ width: 1.5em;
+ height: 1em;
+ position: relative;
+ top: 0.1em;
+}
+
+.link-vjs a:before {
+ background: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3e%3c!--! Font Awesome Free 6.2.1 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --%3e%3cpath d='M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z'/%3e%3c/svg%3e") no-repeat;
+}
+
+.link-gh a:before {
+ background: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 480 512'%3e%3c!--! Font Awesome Free 6.2.1 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --%3e%3cpath d='M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z'/%3e%3c/svg%3e") no-repeat;
+}
+
+.link-tw a:before {
+ background: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3c!--! Font Awesome Free 6.2.1 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --%3e%3cpath d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'/%3e%3c/svg%3e") no-repeat;
+}
+
+.light .navbar {
+ background-color: #f7f7f7;
+ border-radius: 1rem;
+
+}
+
+.dark .navbar {
+ background-color: #222;
+ border-radius: 1rem;
+} \ No newline at end of file
diff --git a/js/videojs/build/fix-api-docs.js b/js/videojs/build/fix-api-docs.js
new file mode 100644
index 0000000..928a132
--- /dev/null
+++ b/js/videojs/build/fix-api-docs.js
@@ -0,0 +1,43 @@
+const replace = require('replace');
+const path = require('path');
+const apiPath = path.join(__dirname, '..', 'docs', 'api');
+
+const replacements = [
+ {find: /\/docs\/guides\/(.+)\.md/g, replace: 'tutorial-$1.html'},
+ {find: /tutorial-tech.html/g, replace: 'tutorial-tech_.html'},
+ {find: /\/docs\/guides\//g, replace: '#'},
+ {find: /(\<h[1-6] id="(?:.*)?)video-js(.*)?"\>/g, replace: '$1videojs$2">'},
+ {find: /(\<h[1-6] id="(?:.*)?)don-t(.*)?"\>/g, replace: '$1dont$2">'},
+ {find: /(\<h[1-6] id="(?:.*)?)node-js(.*)?"\>/g, replace: '$1nodejs$2">'},
+ {find: /(\<h[1-6] id="(?:.*)?)vtt-js(.*)?"\>/g, replace: '$1vttjs$2">'},
+ {find: /(\<h[1-6] id=")-(.*)("\>)/g, replace: '$1$2$3'},
+ {find: /(\<h[1-6] id=")(.*)-("\>)/g, replace: '$1$2$3'},
+ {find: /(\<h[1-6] id=".*)-docs-guides-.*-md("\>)/g, replace: '$1$2'},
+ // replace all children with children-1
+ {find: /\<h3 id="children"\>/g, replace: '<h3 id="children-1">'},
+ // remove the -1 from the first item
+ {find: /\<h3 id="children-1"\>/, replace: '<h3 id="children">'},
+ {find: '<h4 id="nativecontrolsfortouch">', replace: '<h4 id="nativecontrolsfortouch-1">'},
+ {find: '<h3 id="videojs-(audio|video)track">', replace: '<h3 id="videojs$1track">'},
+ {find: '<h3 id="text-tracks">', replace: '<h3 id="text-tracks-1">'},
+ {find: '<h2 id="q-how-can-i-hide-the-links-to-my-video-subtitles-audio-tracks">',
+ replace: '<h2 id="q-how-can-i-hide-the-links-to-my-videosubtitlesaudiotracks">'},
+ {find: '<h3 id="dispose-http-docs-videojs-com-player-html-dispose">',
+ replace: '<h3 id="dispose">'},
+ {find: '<h4 id="effect-on-player-width-and-player-height">',
+ replace: '<h4 id="effect-on-playerwidth-and-playerheight">'},
+ {find: '<h4 id="i-want-to-have-a-single-source-and-dont-care-about-live-adaptive-streaming">',
+ replace: '<h4 id="i-want-to-have-a-single-source-and-dont-care-about-liveadaptive-streaming">'},
+ {find: '<h2 id="api-docs-api">', replace: '<h2 id="api-docs">'},
+ {find: '<h2 id="guides-docs-guides">', replace: '<h2 id="guides">'}
+];
+
+replacements.forEach(function(obj) {
+ replace({
+ regex: obj.find,
+ replacement: obj.replace,
+ paths: [apiPath],
+ recursive: true,
+ silent: true
+ });
+});
diff --git a/js/videojs/build/jsdoc-typeof-plugin.js b/js/videojs/build/jsdoc-typeof-plugin.js
new file mode 100644
index 0000000..7056482
--- /dev/null
+++ b/js/videojs/build/jsdoc-typeof-plugin.js
@@ -0,0 +1,12 @@
+/**
+ * The jsdoc plugin that will convert types like {typeof ClassName} into {Class<ClassName>}
+ */
+exports.handlers = {
+ jsdocCommentFound: event => {
+ event.comment = (event.comment || '').replace(/\{.*typeof\s+([^\s\|]+).*\}/g, typeExpression => {
+ return typeExpression.replace(/typeof\s+([^\s\|\}]+)/g, (expression, className) => {
+ return 'Class<' + className + '>';
+ });
+ });
+ }
+};
diff --git a/js/videojs/build/jsdoc-workarounds.js b/js/videojs/build/jsdoc-workarounds.js
new file mode 100644
index 0000000..4b4272b
--- /dev/null
+++ b/js/videojs/build/jsdoc-workarounds.js
@@ -0,0 +1,13 @@
+/**
+ * This jsdoc plugin works around some typescript-flavoured jsdoc that isn't actual jsdoc,
+ * so docs:api doesn't fail
+ */
+exports.handlers = {
+ jsdocCommentFound: event => {
+ // Special case for media-error.js
+ event.comment = (event.comment || '').replace(
+ '@typedef {{errorType: string, [key: string]: any}} ErrorMetadata',
+ '@typedef {Object} ErrorMetadata\n * @property {string} errorType Error type'
+ );
+ }
+};
diff --git a/js/videojs/build/license-header.txt b/js/videojs/build/license-header.txt
new file mode 100644
index 0000000..45efc67
--- /dev/null
+++ b/js/videojs/build/license-header.txt
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Video.js <%= version %> <http://videojs.com/>
+ * <%= copyright %>
+ * Available under Apache License Version 2.0
+ * <https://github.com/videojs/video.js/blob/main/LICENSE>
+<% if (includesVtt) { %> *
+ * Includes vtt.js <https://github.com/mozilla/vtt.js>
+ * Available under Apache License Version 2.0
+ * <https://github.com/mozilla/vtt.js/blob/main/LICENSE>
+<% } %> */
diff --git a/js/videojs/build/minify.js b/js/videojs/build/minify.js
new file mode 100644
index 0000000..2576777
--- /dev/null
+++ b/js/videojs/build/minify.js
@@ -0,0 +1,48 @@
+/* eslint-disable no-console, camelcase */
+
+const fs = require('fs');
+const uglify = require('uglify-js');
+const maxmin = require('maxmin');
+
+const options = {
+ nameCache: {},
+ output: {
+ comments: 'some'
+ },
+ mangle: true,
+ compress: {
+ sequences: true,
+ dead_code: true,
+ conditionals: true,
+ booleans: true,
+ unused: true,
+ if_return: true,
+ join_vars: true,
+ drop_console: true,
+ typeofs: false
+ }
+};
+
+const minify = (file, dest) => {
+ const code = fs.readFileSync(file, 'utf8');
+ const minified = uglify.minify(code, options);
+
+ if (minified.error) {
+ console.error(minified.error);
+ return;
+ }
+
+ if (minified.warnings) {
+ console.warn(minified.warnings);
+ }
+
+ fs.writeFileSync(dest, minified.code, 'utf8');
+ console.log('File', dest, 'created:', maxmin(code, minified.code, true));
+};
+
+console.log('Minifying files\n');
+
+minify('dist/video.js', 'dist/video.min.js');
+minify('dist/alt/video.novtt.js', 'dist/alt/video.novtt.min.js');
+minify('dist/alt/video.core.js', 'dist/alt/video.core.min.js');
+minify('dist/alt/video.core.novtt.js', 'dist/alt/video.core.novtt.min.js');
diff --git a/js/videojs/build/netlify-docs.js b/js/videojs/build/netlify-docs.js
new file mode 100644
index 0000000..e7ef6ad
--- /dev/null
+++ b/js/videojs/build/netlify-docs.js
@@ -0,0 +1,23 @@
+const sh = require('shelljs');
+const semver = require('semver');
+const path = require('path');
+
+const GIT_LOG = `git log --format=%B -n 1 ${process.env.COMMIT_REF}`;
+const output = sh.exec(GIT_LOG, {async: false, silent: true}).stdout;
+
+// if we're on main branch and not on a tagged commit,
+// error the build so it doesn't redeploy the docs
+if (process.env.BRANCH === 'main' && semver.valid(output.trim()) === null) {
+ process.exit(1);
+} else {
+ sh.exec('npm run docs:api');
+ sh.cp('-R', 'docs/legacy-docs', 'docs/api/docs');
+
+ // move docs/_redirects into the root of the docs site
+ //
+ // this is needed because the root of the docs site is docs/api, which is not
+ // in version control.
+ const docsPath = path.join(__dirname, '..', 'docs');
+
+ sh.cp(path.join(docsPath, '_redirects'), path.join(docsPath, 'api', '_redirects'));
+}
diff --git a/js/videojs/build/netlify.js b/js/videojs/build/netlify.js
new file mode 100644
index 0000000..2e3896c
--- /dev/null
+++ b/js/videojs/build/netlify.js
@@ -0,0 +1,38 @@
+const pkg = require('../package.json');
+const path = require('path');
+const sh = require('shelljs');
+
+process.env.CI = true;
+// run build steps
+sh.exec('npm run build');
+sh.exec('npm run sandbox');
+sh.exec('npm run docs:api');
+
+// copy the legacy docs over
+sh.cp('-R', 'docs/legacy-docs', 'docs/api/docs');
+
+const deployDir = 'deploy';
+const files = [
+ 'node_modules/es5-shim/es5-shim.js',
+ 'node_modules/es6-shim/es6-shim.js'
+];
+
+// cleanup previous deploy
+sh.rm('-rf', deployDir);
+// make sure the directory exists
+sh.mkdir('-p', deployDir);
+
+// create sub-directory for images
+sh.mkdir('-p', `${deployDir}/src`);
+
+// create nested directories
+files
+ .map((file) => path.dirname(file))
+ .forEach((dir) => sh.mkdir('-p', path.join(deployDir, dir)));
+
+// copy files/folders to deploy dir
+files
+ .concat('dist', 'index.html', 'sandbox', 'docs', 'src/images')
+ .forEach((file) => sh.cp('-r', file, path.join(deployDir, file)));
+
+sh.rm(path.join(deployDir, 'dist', `video-js-${pkg.version}.zip`));
diff --git a/js/videojs/build/readme-version.js b/js/videojs/build/readme-version.js
new file mode 100644
index 0000000..2942c81
--- /dev/null
+++ b/js/videojs/build/readme-version.js
@@ -0,0 +1,16 @@
+/*
+Replaces the version number in the readme with the current package version.
+Looks for patterns like `/8.17.3/` and `/video.js@8.17.3/`
+*/
+
+const fs = require('fs');
+const path = require('path');
+const version = require('../package.json').version;
+
+let doc = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8');
+
+doc = doc
+ .replace(/\/video.js@\d\.\d+\.\d+\//g, `/video.js@${version}/`)
+ .replace(/\/\d\.\d+\.\d+\//g, `/${version}/`);
+
+fs.writeFileSync(path.join(__dirname, '..', 'README.md'), doc, 'utf8');
diff --git a/js/videojs/build/rollup-exclude-lines.js b/js/videojs/build/rollup-exclude-lines.js
new file mode 100644
index 0000000..a9304f7
--- /dev/null
+++ b/js/videojs/build/rollup-exclude-lines.js
@@ -0,0 +1,41 @@
+/**
+ * Remove parts of files from outputs. Everything between a pair of `/* start-delete-from-build *\u002f`
+ * and `/* end-delete-from-build *\u002f` comments
+ *
+ * Based on https://github.com/se-panfilov/rollup-plugin-strip-code
+ */
+
+import { createFilter } from '@rollup/pluginutils';
+
+const START_COMMENT = 'start-delete-from-build';
+const END_COMMENT = 'end-delete-from-build';
+
+/**
+ * Remove lines of code surrounded by comments
+ *
+ * @param {Object} [options] Options
+ * @param {string} [options.include] Files to inlcude
+ * @param {string} [options.exclude] Files to exclude
+ * @param {string} [options.startComment] Starting keywork, default start-delete-from-build
+ * @param {string} [options.endComment] Eding keywork, default end-delete-from-build
+ * @param {RegExp} [options.pattern] Custom regex
+ * @return void
+ */
+export default function excludeLines(options = {}) {
+ // assume that the myPlugin accepts options of `options.include` and `options.exclude`
+ const filter = createFilter(options.include, options.exclude);
+
+ return {
+ transform(code, id) {
+ if (!filter(id)) {
+ return;
+ }
+
+ const startComment = options.startComment || START_COMMENT;
+ const endComment = options.endComment || END_COMMENT;
+ const defaultPattern = new RegExp(`([\\t ]*\\/\\* ?${startComment} ?\\*\\/)[\\s\\S]*?(\\/\\* ?${endComment} ?\\*\\/[\\t ]*\\n?)`, 'g');
+
+ return code.replace(options.pattern || defaultPattern, '');
+ }
+ };
+}
diff --git a/js/videojs/build/sandbox.js b/js/videojs/build/sandbox.js
new file mode 100644
index 0000000..0da0b89
--- /dev/null
+++ b/js/videojs/build/sandbox.js
@@ -0,0 +1,31 @@
+/* eslint-disable no-console */
+
+const fs = require('fs');
+const path = require('path');
+const sh = require('shelljs');
+
+const files = sh.find(path.join(__dirname, '..', 'sandbox', '**', '*.*'))
+ .filter((filepath) => path.extname(filepath) === '.example');
+
+const changes = files.map(function(filepath) {
+ const p = path.parse(filepath);
+ const nonExample = path.join(p.dir, p.name);
+
+ return {
+ file: filepath,
+ copy: nonExample
+ };
+}).filter(function(change) {
+ return !fs.existsSync(change.copy);
+});
+
+changes.forEach(function(change) {
+ fs.copyFileSync(change.file, change.copy);
+});
+
+if (changes.length) {
+ console.log('Updated Sandbox files for:');
+ console.log('\t' + changes.map((chg) => chg.copy).join('\n\t'));
+} else {
+ console.log('No sandbox updates necessary');
+}
diff --git a/js/videojs/build/test-a11y.js b/js/videojs/build/test-a11y.js
new file mode 100644
index 0000000..9debe18
--- /dev/null
+++ b/js/videojs/build/test-a11y.js
@@ -0,0 +1,27 @@
+const AccessSniff = require('access-sniff');
+const path = require('path');
+
+const testFiles = [
+ path.join(__dirname, '..', 'sandbox', 'descriptions.test-a11y.html')
+];
+
+const options = {
+ accessibilityLevel: 'WCAG2AA',
+ reportLevels: {
+ notice: false,
+ warning: true,
+ error: true
+ },
+ ignore: [
+ // Ignore warning about contrast of the "vjs-no-js" fallback link
+ 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.BgImage'
+ ]
+};
+
+AccessSniff.default(testFiles, options).then(function(report) {
+ AccessSniff.report(report);
+}).catch(function() {
+
+ // there were errors, which are already reported, exit with an error
+ process.exit(1);
+});
diff --git a/js/videojs/build/translations.js b/js/videojs/build/translations.js
new file mode 100644
index 0000000..f74fe53
--- /dev/null
+++ b/js/videojs/build/translations.js
@@ -0,0 +1,54 @@
+/* eslint-disable no-console */
+
+const fs = require('fs');
+const path = require('path');
+const sh = require('shelljs');
+
+const source = require('../lang/en.json');
+const table = require('markdown-table');
+const tableRegex = /(<!-- START langtable -->)(.|\n)*(<!-- END langtable -->)/;
+
+let doc = fs.readFileSync(path.join(__dirname, '..', 'docs', 'translations-needed.md'), 'utf8');
+const tableData = [['Language file', 'Missing translations']];
+
+const filepaths = sh.find(path.join(__dirname, '..', 'lang', '**', '!(zh-Hans|zh-Hant)*.json'));
+
+filepaths.forEach((filepath) => {
+ const filename = path.basename(filepath);
+
+ if (filename === 'en.json') {
+ return;
+ }
+
+ const target = require(filepath);
+
+ // Special case for English, the assumption being that since the keys are English only
+ // a few strings need to be altered for regional differences, e.g. en-GB.
+ if (filename.startsWith('en-')) {
+ console.log(`${filename} English - should be manually checked.`);
+ tableData.push([`${filename} (has ${Object.keys(target).length})`, 'Needs manual checking. Can safely use most default strings.']);
+ return;
+ }
+
+ const missing = [];
+
+ for (const string in source) {
+ if (!target[string]) {
+ console.log(`${filename} missing "${string}"`);
+ missing.push(string);
+ }
+ }
+ if (missing.length > 0) {
+ console.error(`${filename} is missing ${missing.length} translations.`);
+ tableData.push([`${filename} (missing ${missing.length})`, missing[0]]);
+ for (let i = 1; i < missing.length; i++) {
+ tableData.push(['', missing[i]]);
+ }
+ } else {
+ console.log(`${filename} is up to date.`);
+ tableData.push([`${filename} (Complete)`, '']);
+ }
+});
+
+doc = doc.replace(tableRegex, '$1\n' + table(tableData) + '\n$3');
+fs.writeFileSync(path.join(__dirname, '..', 'docs', 'translations-needed.md'), doc, 'utf8');