├── .nvmrc ├── .npmignore ├── .editorconfig ├── scripts ├── karma.conf.js └── rollup.config.js ├── .travis.yml ├── .gitignore ├── test ├── setup.test.js ├── player-script-cache.test.js ├── urls.test.js ├── index.test.js └── create-embed.test.js ├── LICENSE ├── src ├── utils │ └── environment.js ├── constants.js ├── urls.js ├── env.js ├── player-script-cache.js ├── create-embed.js └── index.js ├── CONTRIBUTING.md ├── package.json ├── CHANGELOG.md ├── demo.js ├── index.html └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /scripts/karma.conf.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-karma-config'); 2 | 3 | module.exports = function(config) { 4 | 5 | // see https://github.com/videojs/videojs-generate-karma-config 6 | // for options 7 | const options = {}; 8 | 9 | config = generate(config, options); 10 | 11 | // any other custom stuff not supported by options here! 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: node_js 4 | # node version is specified using the .nvmrc file 5 | before_install: 6 | - npm install -g greenkeeper-lockfile@1 7 | before_script: 8 | - export DISPLAY=:99.0 9 | - sh -e /etc/init.d/xvfb start 10 | - greenkeeper-lockfile-update 11 | after_script: 12 | - greenkeeper-lockfile-upload 13 | addons: 14 | firefox: latest 15 | chrome: stable 16 | 17 | -------------------------------------------------------------------------------- /.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 | # Build-related directories 29 | dist/ 30 | docs/api/ 31 | test/dist/ 32 | .eslintcache 33 | .yo-rc.json 34 | -------------------------------------------------------------------------------- /test/setup.test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'qunit'; 2 | import {getWindow} from '../src/utils/environment'; 3 | 4 | // Global test setup to prevent page reloads during tests 5 | QUnit.begin(() => { 6 | // Prevent page reloads during tests 7 | const window = getWindow(); 8 | 9 | window.onbeforeunload = () => 'Preventing reload during tests'; 10 | }); 11 | 12 | QUnit.done(() => { 13 | // Clean up the onbeforeunload handler 14 | const window = getWindow(); 15 | 16 | window.onbeforeunload = null; 17 | }); 18 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-rollup-config'); 2 | 3 | // see https://github.com/videojs/videojs-generate-rollup-config 4 | // for options 5 | const options = { 6 | input: 'src/index.js', 7 | distName: 'brightcove-player-loader', 8 | exportName: 'brightcovePlayerLoader', 9 | testInput: ['test/setup.test.js', 'test/**/*.test.js'] 10 | }; 11 | const config = generate(options); 12 | 13 | // Add additonal builds/customization here! 14 | 15 | // export the builds to rollup 16 | export default Object.values(config.builds); 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Brightcove, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/utils/environment.js: -------------------------------------------------------------------------------- 1 | /* global globalThis window self document */ 2 | 3 | const getGlobal = () => { 4 | if (typeof globalThis !== 'undefined') { 5 | return globalThis; 6 | } 7 | 8 | if (typeof window !== 'undefined') { 9 | return window; 10 | } 11 | 12 | if (typeof self !== 'undefined') { 13 | return self; 14 | } 15 | 16 | if (typeof global !== 'undefined') { 17 | return global; 18 | } 19 | 20 | return {}; 21 | }; 22 | 23 | const getWindow = () => { 24 | if (typeof window !== 'undefined') { 25 | return window; 26 | } 27 | 28 | const globalObject = getGlobal(); 29 | 30 | if (globalObject.window && globalObject.window.window === globalObject.window) { 31 | return globalObject.window; 32 | } 33 | 34 | return globalObject; 35 | }; 36 | 37 | const getDocument = () => { 38 | if (typeof document !== 'undefined') { 39 | return document; 40 | } 41 | 42 | const win = getWindow(); 43 | 44 | if (win && win.document) { 45 | return win.document; 46 | } 47 | 48 | throw new Error('document is not available in this environment'); 49 | }; 50 | 51 | export { 52 | getDocument, 53 | getGlobal, 54 | getWindow 55 | }; 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We welcome contributions from everyone! 4 | 5 | ## Getting Started 6 | 7 | Make sure you have Node.js 4.8 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 conventions][conventions] 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 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md 31 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | import {getWindow} from './utils/environment'; 2 | 3 | const window = getWindow(); 4 | 5 | const DEFAULTS = { 6 | embedId: 'default', 7 | embedType: 'in-page', 8 | playerId: 'default', 9 | Promise: window.Promise, 10 | refNodeInsert: 'append' 11 | }; 12 | 13 | const DEFAULT_ASPECT_RATIO = '16:9'; 14 | const DEFAULT_IFRAME_HORIZONTAL_PLAYLIST = false; 15 | const DEFAULT_MAX_WIDTH = '100%'; 16 | 17 | const EMBED_TAG_NAME_VIDEO = 'video'; 18 | const EMBED_TAG_NAME_VIDEOJS = 'video-js'; 19 | 20 | const EMBED_TYPE_IN_PAGE = 'in-page'; 21 | const EMBED_TYPE_IFRAME = 'iframe'; 22 | 23 | const REF_NODE_INSERT_APPEND = 'append'; 24 | const REF_NODE_INSERT_PREPEND = 'prepend'; 25 | const REF_NODE_INSERT_BEFORE = 'before'; 26 | const REF_NODE_INSERT_AFTER = 'after'; 27 | const REF_NODE_INSERT_REPLACE = 'replace'; 28 | 29 | const JSON_ALLOWED_ATTRS = ['catalogSearch', 'catalogSequence']; 30 | 31 | export { 32 | DEFAULTS, 33 | DEFAULT_ASPECT_RATIO, 34 | DEFAULT_IFRAME_HORIZONTAL_PLAYLIST, 35 | DEFAULT_MAX_WIDTH, 36 | EMBED_TAG_NAME_VIDEO, 37 | EMBED_TAG_NAME_VIDEOJS, 38 | EMBED_TYPE_IN_PAGE, 39 | EMBED_TYPE_IFRAME, 40 | REF_NODE_INSERT_APPEND, 41 | REF_NODE_INSERT_PREPEND, 42 | REF_NODE_INSERT_BEFORE, 43 | REF_NODE_INSERT_AFTER, 44 | REF_NODE_INSERT_REPLACE, 45 | JSON_ALLOWED_ATTRS 46 | }; 47 | -------------------------------------------------------------------------------- /src/urls.js: -------------------------------------------------------------------------------- 1 | import brightcovePlayerUrl from '@brightcove/player-url'; 2 | import {EMBED_TYPE_IFRAME} from './constants'; 3 | 4 | let BASE_URL = 'https://players.brightcove.net/'; 5 | 6 | /** 7 | * Gets the URL to a player on CDN. 8 | * 9 | * @private 10 | * @param {Object} params 11 | * A parameters object. See README for details. 12 | * 13 | * @return {string} 14 | * A URL. 15 | */ 16 | const getUrl = (params) => { 17 | 18 | if (params.playerUrl) { 19 | return params.playerUrl; 20 | } 21 | 22 | const {accountId, playerId, embedId, embedOptions} = params; 23 | const iframe = params.embedType === EMBED_TYPE_IFRAME; 24 | 25 | return brightcovePlayerUrl({ 26 | accountId, 27 | playerId, 28 | embedId, 29 | iframe, 30 | base: BASE_URL, 31 | 32 | // The unminified embed option is the exact reverse of the minified option 33 | // here. 34 | minified: embedOptions ? !embedOptions.unminified : true, 35 | 36 | // Pass the entire params object as query params. This is safe because 37 | // @brightcove/player-url only accepts a whitelist of parameters. Anything 38 | // else will be ignored. 39 | queryParams: params 40 | }); 41 | }; 42 | 43 | /** 44 | * Function used to get the base URL - primarily for testing. 45 | * 46 | * @private 47 | * @return {string} 48 | * The current base URL. 49 | */ 50 | const getBaseUrl = () => BASE_URL; 51 | 52 | /** 53 | * Function used to set the base URL - primarily for testing. 54 | * 55 | * @private 56 | * @param {string} baseUrl 57 | * A new base URL (instead of Brightcove CDN). 58 | */ 59 | const setBaseUrl = (baseUrl) => { 60 | BASE_URL = baseUrl; 61 | }; 62 | 63 | export default { 64 | getUrl, 65 | getBaseUrl, 66 | setBaseUrl 67 | }; 68 | -------------------------------------------------------------------------------- /test/player-script-cache.test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'qunit'; 2 | import playerScriptCache from '../src/player-script-cache'; 3 | 4 | QUnit.module('playerScriptCache', function(hooks) { 5 | 6 | hooks.afterEach(function() { 7 | playerScriptCache.clear(); 8 | }); 9 | 10 | QUnit.test('key', function(assert) { 11 | assert.strictEqual(playerScriptCache.key({}), '*_undefined_undefined'); 12 | assert.strictEqual(playerScriptCache.key({playerId: '1', embedId: '2'}), '*_1_2'); 13 | assert.strictEqual(playerScriptCache.key({accountId: '0', playerId: '1', embedId: '2'}), '0_1_2'); 14 | }); 15 | 16 | QUnit.test('store/has/get', function(assert) { 17 | const a = {}; 18 | const b = {playerId: '1', embedId: '2'}; 19 | const c = {accountId: '0', playerId: '1', embedId: '2'}; 20 | 21 | assert.notOk(playerScriptCache.has(a)); 22 | playerScriptCache.store(a); 23 | assert.ok(playerScriptCache.has(a)); 24 | assert.strictEqual(playerScriptCache.get(a), ''); 25 | 26 | assert.notOk(playerScriptCache.has(b)); 27 | playerScriptCache.store(b); 28 | assert.ok(playerScriptCache.has(b)); 29 | assert.strictEqual(playerScriptCache.get(b), ''); 30 | 31 | assert.notOk(playerScriptCache.has(c)); 32 | playerScriptCache.store(c); 33 | assert.ok(playerScriptCache.has(c)); 34 | assert.strictEqual(playerScriptCache.get(c), 'https://players.brightcove.net/0/1_2/index.min.js'); 35 | }); 36 | 37 | QUnit.test('clear', function(assert) { 38 | const a = {playerId: '1', embedId: '2'}; 39 | const b = {accountId: '0', playerId: '1', embedId: '2'}; 40 | 41 | playerScriptCache.store(a); 42 | playerScriptCache.store(b); 43 | playerScriptCache.clear(); 44 | 45 | assert.notOk(playerScriptCache.has(a)); 46 | assert.notOk(playerScriptCache.has(b)); 47 | }); 48 | 49 | QUnit.test('forEach', function(assert) { 50 | const a = {playerId: '1', embedId: '2'}; 51 | const b = {accountId: '0', playerId: '1', embedId: '2'}; 52 | const iterations = []; 53 | 54 | playerScriptCache.store(a); 55 | playerScriptCache.store(b); 56 | playerScriptCache.forEach((value, key) => { 57 | iterations.push([value, key]); 58 | }); 59 | 60 | assert.deepEqual(iterations, [ 61 | ['', '*_1_2'], 62 | ['https://players.brightcove.net/0/1_2/index.min.js', '0_1_2'] 63 | ]); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/urls.test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'qunit'; 2 | import urls from '../src/urls'; 3 | 4 | QUnit.module('urls'); 5 | 6 | QUnit.test('getUrl for in-page embed', function(assert) { 7 | const url = urls.getUrl({ 8 | accountId: '1', 9 | playerId: '2', 10 | embedId: '3' 11 | }); 12 | 13 | assert.strictEqual(url, 'https://players.brightcove.net/1/2_3/index.min.js', 'the URL is correct'); 14 | }); 15 | 16 | QUnit.test('getUrl for iframe embed', function(assert) { 17 | const url = urls.getUrl({ 18 | accountId: '1', 19 | playerId: '2', 20 | embedId: '3', 21 | embedType: 'iframe' 22 | }); 23 | 24 | assert.strictEqual(url, 'https://players.brightcove.net/1/2_3/index.html', 'the URL is correct'); 25 | }); 26 | 27 | QUnit.test('getUrl for iframe embed supports playlistId, playlistVideoId, and videoId as query parameters', function(assert) { 28 | const url = urls.getUrl({ 29 | accountId: '1', 30 | playerId: '2', 31 | embedId: '3', 32 | embedType: 'iframe', 33 | playlistId: 'a', 34 | playlistVideoId: 'b', 35 | videoId: 'c' 36 | }); 37 | 38 | assert.strictEqual(url, 'https://players.brightcove.net/1/2_3/index.html?playlistId=a&playlistVideoId=b&videoId=c', 'the URL is correct'); 39 | }); 40 | 41 | QUnit.test('getUrl for in-page embed DOES NOT support playlistId, playlistVideoId, and videoId as query parameters', function(assert) { 42 | const url = urls.getUrl({ 43 | accountId: '1', 44 | playerId: '2', 45 | embedId: '3', 46 | playlistId: 'a', 47 | playlistVideoId: 'b', 48 | videoId: 'c' 49 | }); 50 | 51 | assert.strictEqual(url, 'https://players.brightcove.net/1/2_3/index.min.js', 'the URL is correct'); 52 | }); 53 | 54 | QUnit.test('getUrl encodes all possible URL components', function(assert) { 55 | const url = urls.getUrl({ 56 | accountId: ';', 57 | playerId: ',', 58 | embedId: '/', 59 | embedType: 'iframe', 60 | playlistId: '?', 61 | playlistVideoId: ':', 62 | videoId: '@' 63 | }); 64 | 65 | assert.strictEqual(url, 'https://players.brightcove.net/%3B/%2C_%2F/index.html?playlistId=%3F&playlistVideoId=%3A&videoId=%40', 'the URL is correct'); 66 | }); 67 | 68 | QUnit.test('getUrl uses playerUrl if it exists', function(assert) { 69 | const url = urls.getUrl({playerUrl: 'something!'}); 70 | 71 | assert.strictEqual(url, 'something!', 'the URL is correct'); 72 | }); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@brightcove/player-loader", 3 | "version": "1.8.1", 4 | "description": "An asynchronous script loader for the Brightcove Player.", 5 | "main": "dist/brightcove-player-loader.cjs.js", 6 | "module": "dist/brightcove-player-loader.es.js", 7 | "generator-videojs-plugin": { 8 | "version": "7.4.0" 9 | }, 10 | "browserslist": [ 11 | "defaults", 12 | "ie 11" 13 | ], 14 | "scripts": { 15 | "prebuild": "npm run clean", 16 | "build": "npm-run-all -p build:*", 17 | "build:js": "rollup -c scripts/rollup.config.js", 18 | "clean": "shx rm -rf ./dist ./test/dist", 19 | "postclean": "shx mkdir -p ./dist ./test/dist", 20 | "lint": "vjsstandard", 21 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch", 22 | "start": "npm-run-all -p server watch", 23 | "pretest": "npm-run-all lint build", 24 | "test": "npm-run-all test:*", 25 | "posttest": "shx cat test/dist/coverage/text.txt", 26 | "test:unit": "karma start scripts/karma.conf.js", 27 | "test:verify": "vjsverify --verbose", 28 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s", 29 | "preversion": "npm test", 30 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md", 31 | "watch": "npm-run-all -p watch:*", 32 | "watch:js": "npm run build:js -- -w", 33 | "prepublishOnly": "npm-run-all build test:verify" 34 | }, 35 | "keywords": [ 36 | "audio", 37 | "brightcove", 38 | "loader", 39 | "media", 40 | "player", 41 | "video" 42 | ], 43 | "author": "Brightcove, Inc.", 44 | "license": "Apache-2.0", 45 | "vjsstandard": { 46 | "ignore": [ 47 | "dist", 48 | "docs", 49 | "test/dist", 50 | "vendor", 51 | "demo.js" 52 | ] 53 | }, 54 | "files": [ 55 | "CONTRIBUTING.md", 56 | "dist/", 57 | "docs/", 58 | "demo.js", 59 | "index.html", 60 | "scripts/", 61 | "src/", 62 | "test/" 63 | ], 64 | "dependencies": { 65 | "@brightcove/player-url": "^1.2.0" 66 | }, 67 | "devDependencies": { 68 | "conventional-changelog-cli": "^2.0.21", 69 | "conventional-changelog-videojs": "^3.0.0", 70 | "doctoc": "^1.3.1", 71 | "husky": "^2.3.0", 72 | "in-publish": "^2.0.0", 73 | "karma": "^6.4.4", 74 | "lint-staged": "^8.1.7", 75 | "not-prerelease": "^1.0.1", 76 | "npm-merge-driver-install": "^1.0.0", 77 | "npm-run-all": "^4.1.5", 78 | "rollup": "^1.12.4", 79 | "shx": "^0.3.2", 80 | "sinon": "^6.1.5", 81 | "videojs-generate-karma-config": "~4.0.2", 82 | "videojs-generate-rollup-config": "^3.2.0", 83 | "videojs-generator-verify": "^1.2.0", 84 | "videojs-standard": "^8.0.3" 85 | }, 86 | "husky": { 87 | "hooks": { 88 | "pre-commit": "lint-staged" 89 | } 90 | }, 91 | "lint-staged": { 92 | "*.js": [ 93 | "vjsstandard --fix", 94 | "git add" 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | import {getDocument, getWindow} from './utils/environment'; 2 | import playerScriptCache from './player-script-cache'; 3 | 4 | const window = getWindow(); 5 | 6 | const REGEX_PLAYER_EMBED = /^([A-Za-z0-9]+)_([A-Za-z0-9]+)$/; 7 | 8 | /** 9 | * Gets an array of current per-player/per-embed `bc` globals that are 10 | * attached to the `bc` global (e.g. `bc.abc123xyz_default`). 11 | * 12 | * If `bc` is not defined, returns an empty array. 13 | * 14 | * @private 15 | * @return {string[]} 16 | * An array of keys. 17 | */ 18 | const getBcGlobalKeys = () => 19 | window.bc ? Object.keys(window.bc).filter(k => REGEX_PLAYER_EMBED.test(k)) : []; 20 | 21 | /** 22 | * Gets known global object keys that Brightcove Players may create. 23 | * 24 | * @private 25 | * @return {string[]} 26 | * An array of global variables that were added during testing. 27 | */ 28 | const getGlobalKeys = () => 29 | Object.keys(window).filter(k => (/^videojs/i).test(k) || (/^(bc)$/).test(k)); 30 | 31 | /** 32 | * Dispose all players from a copy of Video.js. 33 | * 34 | * @param {Function} videojs 35 | * A copy of Video.js. 36 | */ 37 | const disposeAll = (videojs) => { 38 | if (!videojs) { 39 | return; 40 | } 41 | Object.keys(videojs.players).forEach(k => { 42 | const p = videojs.players[k]; 43 | 44 | if (p) { 45 | p.dispose(); 46 | } 47 | }); 48 | }; 49 | 50 | /** 51 | * Resets environment state. 52 | * 53 | * This will dispose ALL Video.js players on the page and remove ALL `bc` and 54 | * `videojs` globals it finds. 55 | */ 56 | const reset = () => { 57 | 58 | // Remove all script elements from the DOM. 59 | playerScriptCache.forEach((value, key) => { 60 | 61 | // If no script URL is associated, skip it. 62 | if (!value) { 63 | return; 64 | } 65 | 66 | // Find all script elements and remove them. 67 | const doc = getDocument(); 68 | 69 | Array.prototype.slice 70 | .call(doc.querySelectorAll(`script[src="${value}"]`)) 71 | .forEach(el => el.parentNode.removeChild(el)); 72 | }); 73 | 74 | // Clear the internal cache that have been downloaded. 75 | playerScriptCache.clear(); 76 | 77 | // Dispose any remaining players from the `videojs` global. 78 | disposeAll(window.videojs); 79 | 80 | // There may be other `videojs` instances lurking in the bowels of the 81 | // `bc` global. This should eliminate any of those. 82 | getBcGlobalKeys().forEach(k => disposeAll(window.bc[k].videojs)); 83 | 84 | // Delete any global object keys that were created. 85 | getGlobalKeys().forEach(k => { 86 | delete window[k]; 87 | }); 88 | }; 89 | 90 | /** 91 | * At runtime, populate the cache with pre-detected players. This allows 92 | * people who have bundled their player or included a script tag before this 93 | * runs to not have to re-download players. 94 | */ 95 | const detectPlayers = () => { 96 | getBcGlobalKeys().forEach(k => { 97 | const matches = k.match(REGEX_PLAYER_EMBED); 98 | const props = {playerId: matches[1], embedId: matches[2]}; 99 | 100 | if (!playerScriptCache.has(props)) { 101 | playerScriptCache.store(props); 102 | } 103 | }); 104 | }; 105 | 106 | export default {detectPlayers, reset}; 107 | -------------------------------------------------------------------------------- /src/player-script-cache.js: -------------------------------------------------------------------------------- 1 | import {getWindow} from './utils/environment'; 2 | import urls from './urls'; 3 | 4 | const window = getWindow(); 5 | 6 | // Tracks previously-downloaded scripts and/or detected players. 7 | // 8 | // The keys follow the format "accountId_playerId_embedId" where accountId is 9 | // optional and defaults to "*". This happens when we detect pre-existing 10 | // player globals. 11 | const actualCache = new window.Map(); 12 | 13 | /** 14 | * Get the cache key given some properties. 15 | * 16 | * @private 17 | * @param {Object} props 18 | * Properties describing the player record to cache. 19 | * 20 | * @param {string} props.playerId 21 | * A player ID. 22 | * 23 | * @param {string} props.embedId 24 | * An embed ID. 25 | * 26 | * @param {string} [props.accountId="*"] 27 | * An optional account ID. This is optional because when we search for 28 | * pre-existing players to avoid downloads, we will not necessarily 29 | * know the account ID. 30 | * 31 | * @return {string} 32 | * A key to be used in the script cache. 33 | */ 34 | const key = ({accountId, playerId, embedId}) => `${accountId || '*'}_${playerId}_${embedId}`; 35 | 36 | /** 37 | * Add an entry to the script cache. 38 | * 39 | * @private 40 | * @param {Object} props 41 | * Properties describing the player record to cache. 42 | * 43 | * @param {string} props.playerId 44 | * A player ID. 45 | * 46 | * @param {string} props.embedId 47 | * An embed ID. 48 | * 49 | * @param {string} [props.accountId="*"] 50 | * An optional account ID. This is optional because when we search for 51 | * pre-existing players to avoid downloads, we will not necessarily 52 | * know the account ID. If not given, we assume that no script was 53 | * downloaded for this player. 54 | */ 55 | const store = (props) => { 56 | actualCache.set(key(props), props.accountId ? urls.getUrl(props) : ''); 57 | }; 58 | 59 | /** 60 | * Checks if the script cache has an entry. 61 | * 62 | * @private 63 | * @param {Object} props 64 | * Properties describing the player record to cache. 65 | * 66 | * @param {string} props.playerId 67 | * A player ID. 68 | * 69 | * @param {string} props.embedId 70 | * An embed ID. 71 | * 72 | * @param {string} [props.accountId="*"] 73 | * An optional account ID. This is optional because when we search for 74 | * pre-existing players to avoid downloads, we will not necessarily 75 | * know the account ID. 76 | * 77 | * @return {boolean} 78 | * Will be `true` if there is a matching cache entry. 79 | */ 80 | const has = (props) => actualCache.has(key(props)); 81 | 82 | /** 83 | * Gets a cache entry. 84 | * 85 | * @private 86 | * @param {Object} props 87 | * Properties describing the player record to cache. 88 | * 89 | * @param {string} props.playerId 90 | * A player ID. 91 | * 92 | * @param {string} props.embedId 93 | * An embed ID. 94 | * 95 | * @param {string} [props.accountId="*"] 96 | * An optional account ID. This is optional because when we search for 97 | * pre-existing players to avoid downloads, we will not necessarily 98 | * know the account ID. 99 | * 100 | * @return {string} 101 | * A cache entry - a URL or empty string. 102 | * 103 | */ 104 | const get = (props) => actualCache.get(key(props)); 105 | 106 | /** 107 | * Clears the cache. 108 | */ 109 | const clear = () => { 110 | actualCache.clear(); 111 | }; 112 | 113 | /** 114 | * Iterates over the cache. 115 | * 116 | * @param {Function} fn 117 | * A callback function that will be called with a value and a key 118 | * for each item in the cache. 119 | */ 120 | const forEach = (fn) => { 121 | actualCache.forEach(fn); 122 | }; 123 | 124 | export default { 125 | clear, 126 | forEach, 127 | get, 128 | has, 129 | key, 130 | store 131 | }; 132 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [1.8.1](https://github.com/brightcove/player-loader/compare/v1.8.0...v1.8.1) (2025-10-15) 3 | 4 | ### Bug Fixes 5 | 6 | * Prevent test page reloads by adding global onbeforeunload handler ([64dbf0e](https://github.com/brightcove/player-loader/commit/64dbf0e)) 7 | 8 | ### Code Refactoring 9 | 10 | * remove global shim (#86) ([4fbebbb](https://github.com/brightcove/player-loader/commit/4fbebbb)), closes [#86](https://github.com/brightcove/player-loader/issues/86) 11 | 12 | 13 | # [1.8.0](https://github.com/brightcove/player-loader/compare/v1.7.1...v1.8.0) (2020-10-09) 14 | 15 | ### Features 16 | 17 | * Add support for poster URL override (#67) ([d341708](https://github.com/brightcove/player-loader/commit/d341708)), closes [#67](https://github.com/brightcove/player-loader/issues/67) 18 | 19 | 20 | ## [1.7.1](https://github.com/brightcove/player-loader/compare/v1.7.0...v1.7.1) (2019-06-21) 21 | 22 | ### Code Refactoring 23 | 24 | * Rename the mis-named `configId` parameter (#52) ([6896897](https://github.com/brightcove/player-loader/commit/6896897)), closes [#52](https://github.com/brightcove/player-loader/issues/52) 25 | 26 | 27 | # [1.7.0](https://github.com/brightcove/player-loader/compare/v1.6.0...v1.7.0) (2019-06-04) 28 | 29 | ### Features 30 | 31 | * Support adConfigId and configId params (#50) ([b33157c](https://github.com/brightcove/player-loader/commit/b33157c)), closes [#50](https://github.com/brightcove/player-loader/issues/50) 32 | 33 | ### Chores 34 | 35 | * **package:** Update outdated dependencies (#49) ([cae135e](https://github.com/brightcove/player-loader/commit/cae135e)), closes [#49](https://github.com/brightcove/player-loader/issues/49) 36 | 37 | 38 | # [1.6.0](https://github.com/brightcove/player-loader/compare/v1.5.0...v1.6.0) (2018-10-23) 39 | 40 | ### Features 41 | 42 | * Add support for an iframeHorizontalPlaylist embed option, enhance playlist embed option to support legacy playlists. (#35) ([09e8694](https://github.com/brightcove/player-loader/commit/09e8694)), closes [#35](https://github.com/brightcove/player-loader/issues/35) 43 | * Replace existing demo pages with one page allowing many configurations. (#34) ([123c7a8](https://github.com/brightcove/player-loader/commit/123c7a8)), closes [#34](https://github.com/brightcove/player-loader/issues/34) 44 | 45 | ### Chores 46 | 47 | * **package:** update videojs-generate-rollup-config to version 2.3.0 ([4f401e5](https://github.com/brightcove/player-loader/commit/4f401e5)) 48 | 49 | 50 | # [1.5.0](https://github.com/brightcove/player-loader/compare/v1.4.2...v1.5.0) (2018-10-05) 51 | 52 | ### Features 53 | 54 | * allow player url to be passed in (#32) ([fc52bc8](https://github.com/brightcove/player-loader/commit/fc52bc8)), closes [#32](https://github.com/brightcove/player-loader/issues/32) 55 | 56 | ### Chores 57 | 58 | * **package:** update videojs-standard to version 8.0.2 (#31) ([9161428](https://github.com/brightcove/player-loader/commit/9161428)), closes [#31](https://github.com/brightcove/player-loader/issues/31) 59 | 60 | 61 | ## [1.4.2](https://github.com/brightcove/player-loader/compare/v1.4.1...v1.4.2) (2018-09-17) 62 | 63 | ### Chores 64 | 65 | * Mark players as having been loaded with this library. (#30) ([e327286](https://github.com/brightcove/player-loader/commit/e327286)), closes [#30](https://github.com/brightcove/player-loader/issues/30) 66 | 67 | 68 | ## [1.4.1](https://github.com/brightcove/player-loader/compare/v1.4.0...v1.4.1) (2018-09-12) 69 | 70 | ### Bug Fixes 71 | 72 | * Fix an issue where embedding a player more than once would result in "Uncaught TypeError: resolve is not a function" (#28) ([2d54995](https://github.com/brightcove/player-loader/commit/2d54995)), closes [#28](https://github.com/brightcove/player-loader/issues/28) 73 | 74 | 75 | # [1.4.0](https://github.com/brightcove/player-loader/compare/v1.3.2...v1.4.0) (2018-09-06) 76 | 77 | ### Features 78 | 79 | * Add `embedOptions.tagName` parameter to improve support for older Brightcove Player versions. (#26) ([c4c10e7](https://github.com/brightcove/player-loader/commit/c4c10e7)), closes [#26](https://github.com/brightcove/player-loader/issues/26) 80 | 81 | 82 | ## [1.3.2](https://github.com/brightcove/player-loader/compare/v1.3.1...v1.3.2) (2018-09-05) 83 | 84 | ### Bug Fixes 85 | 86 | * Remove the postinstall script to prevent install issues ([5fca070](https://github.com/brightcove/player-loader/commit/5fca070)) 87 | 88 | ### Chores 89 | 90 | * **package:** Update dependencies to enable Greenkeeper 🌴 (#20) ([fc9ee13](https://github.com/brightcove/player-loader/commit/fc9ee13)), closes [#20](https://github.com/brightcove/player-loader/issues/20) 91 | 92 | 93 | ## [1.3.1](https://github.com/brightcove/player-loader/compare/v1.3.0...v1.3.1) (2018-08-30) 94 | 95 | ### Chores 96 | 97 | * Update tooling using plugin generator v7.2.0 (#19) ([fa7961b](https://github.com/brightcove/player-loader/commit/fa7961b)), closes [#19](https://github.com/brightcove/player-loader/issues/19) 98 | 99 | 100 | # [1.3.0](https://github.com/brightcove/player-loader/compare/v1.2.1...v1.3.0) (2018-08-28) 101 | 102 | ### Features 103 | 104 | * Allow an embed to use the unminified version of a player. (#14) ([49a1819](https://github.com/brightcove/player-loader/commit/49a1819)), closes [#14](https://github.com/brightcove/player-loader/issues/14) 105 | 106 | ### Bug Fixes 107 | 108 | * Fix an issue with using the refNode param as a string. (#13) ([57f64fa](https://github.com/brightcove/player-loader/commit/57f64fa)), closes [#13](https://github.com/brightcove/player-loader/issues/13) 109 | * Fix usage of the 'replace' value for refNodeInsert. (#15) ([36ba6f2](https://github.com/brightcove/player-loader/commit/36ba6f2)), closes [#15](https://github.com/brightcove/player-loader/issues/15) 110 | 111 | ### Documentation 112 | 113 | * Add note describing which Brightcove Player versions this is compatible with. (#17) ([7301b73](https://github.com/brightcove/player-loader/commit/7301b73)), closes [#17](https://github.com/brightcove/player-loader/issues/17) 114 | 115 | 116 | ## [1.2.1](https://github.com/brightcove/player-loader/compare/v1.2.0...v1.2.1) (2018-08-17) 117 | 118 | ### Code Refactoring 119 | 120 | * Use [@brightcove](https://github.com/brightcove)/player-url to generate URLs. ([253d019](https://github.com/brightcove/player-loader/commit/253d019)) 121 | 122 | 123 | # [1.2.0](https://github.com/brightcove/player-loader/compare/v1.1.0...v1.2.0) (2018-08-14) 124 | 125 | ### Features 126 | 127 | * Detect pre-existing players on the page and cache them to avoid downloading players that may exist before player-loader gets to run. (#10) ([566d4ed](https://github.com/brightcove/player-loader/commit/566d4ed)), closes [#10](https://github.com/brightcove/player-loader/issues/10) 128 | 129 | 130 | # [1.1.0](https://github.com/brightcove/player-loader/compare/v1.0.0...v1.1.0) (2018-08-01) 131 | 132 | ### Features 133 | 134 | * Expose the getUrl function from utils (#9) ([63b18ce](https://github.com/brightcove/player-loader/commit/63b18ce)), closes [#9](https://github.com/brightcove/player-loader/issues/9) 135 | 136 | 137 | # 1.0.0 (2018-07-23) 138 | 139 | ### Features 140 | 141 | * Allow Brightcove Players to be embedded and loaded asynchronously. (#2) ([be708db](https://github.com/brightcove/player-loader/commit/be708db)), closes [#2](https://github.com/brightcove/player-loader/issues/2) 142 | 143 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | window.players = {}; 4 | 5 | var currentPageUrl = window.location.href.split('?')[0]; 6 | var embedCount; 7 | var defaults; 8 | var refNodeRoot = $('#ref-node-root'); 9 | var form = $('#embed-it'); 10 | 11 | var paramFields = { 12 | accountId: $('#account-id'), 13 | adConfigId: $('#ad-config-id'), 14 | applicationId: $('#app-id'), 15 | deliveryConfigId: $('#delivery-config-id'), 16 | embedId: $('#embed-id'), 17 | embedType: $('#embed-type'), 18 | playerId: $('#player-id'), 19 | mediaType: $('#media-type'), 20 | mediaValue: $('#media-value'), 21 | options: $('#vjs-options'), 22 | poster: $('#poster-value') 23 | }; 24 | 25 | var allowedParams = { 26 | accountId: 1, 27 | adConfigId: 1, 28 | applicationId: 1, 29 | catalogSearch: 1, 30 | catalogSequence: 1, 31 | deliveryConfigId: 1, 32 | embedId: 1, 33 | embedOptions: { 34 | pip: 1, 35 | playlist: { 36 | legacy: 1 37 | }, 38 | responsive: { 39 | aspectRatio: 1, 40 | iframeHorizontalPlaylist: 1, 41 | maxWidth: 1 42 | }, 43 | tagName: 1, 44 | unminified: 1 45 | }, 46 | embedType: 1, 47 | playerId: 1, 48 | playlistId: 1, 49 | poster: 1, 50 | videoId: 1 51 | }; 52 | 53 | var embedOptionsFields = { 54 | aspectRatio: $('#eo-resp-ar'), 55 | iframeHorizontalPlaylist: $('#eo-resp-ihp'), 56 | maxWidth: $('#eo-resp-mw'), 57 | pip: $('#eo-pip'), 58 | playlist: $('#eo-playlist'), 59 | responsive: $('#eo-resp'), 60 | tagName: $('#eo-tag-name'), 61 | unminified: $('#eo-unmin') 62 | }; 63 | 64 | function isObj(v) { 65 | return Object.prototype.toString.call(v) === '[object Object]'; 66 | } 67 | 68 | function parseFields(fields) { 69 | return Object.keys(fields).reduce(function(accum, key) { 70 | var field = fields[key]; 71 | 72 | if (field.is(':checkbox')) { 73 | accum[key] = field.is(':checked'); 74 | } else { 75 | accum[key] = (field.val() || field.attr('placeholder') || '').trim(); 76 | } 77 | 78 | return accum; 79 | }, {}); 80 | } 81 | 82 | function normalizeParams(params) { 83 | 84 | // Normalize media type/value 85 | if (params.mediaType && params.mediaValue) { 86 | try { 87 | params[params.mediaType] = JSON.parse(params.mediaValue); 88 | } catch (x) { 89 | params[params.mediaType] = params.mediaValue; 90 | } 91 | } 92 | 93 | delete params.mediaType; 94 | delete params.mediaValue; 95 | 96 | // Normalize Video.js options 97 | if (params.options) { 98 | try { 99 | params.options = JSON.parse(params.options); 100 | } catch (x) { 101 | console.warn('Ignoring invalid JSON in Video.js options!'); 102 | console.error(x); 103 | } 104 | } 105 | 106 | if (!params.options || typeof params.options !== 'object') { 107 | delete params.options; 108 | } 109 | 110 | // Normalize embedOptions 111 | var eo = params.embedOptions; 112 | 113 | if (eo.playlist === 'on') { 114 | eo.playlist = true; 115 | } else if (eo.playlist === 'legacy') { 116 | eo.playlist = {legacy: true}; 117 | } else { 118 | eo.playlist = false; 119 | } 120 | 121 | if (eo.responsive) { 122 | if (eo.iframeHorizontalPlaylist || eo.maxWidth || eo.aspectRatio !== '16:9') { 123 | eo.responsive = { 124 | aspectRatio: eo.aspectRatio 125 | }; 126 | 127 | if (eo.iframeHorizontalPlaylist && params.embedType === 'iframe') { 128 | eo.responsive.iframeHorizontalPlaylist = true; 129 | } 130 | 131 | if (eo.maxWidth) { 132 | eo.responsive.maxWidth = eo.maxWidth; 133 | } 134 | } 135 | } 136 | 137 | delete eo.aspectRatio; 138 | delete eo.iframeHorizontalPlaylist; 139 | delete eo.maxWidth; 140 | 141 | return params; 142 | } 143 | 144 | function filterParams(params, allowed) { 145 | allowed = allowed || allowedParams; 146 | 147 | Object.keys(params).forEach(function(key) { 148 | if (!allowed.hasOwnProperty(key)) { 149 | delete params[key]; 150 | } else if (isObj(params[key]) && isObj(allowed[key])) { 151 | params[key] = filterParams(params[key], allowed[key]); 152 | } 153 | }); 154 | 155 | return params; 156 | } 157 | 158 | function readParams() { 159 | var params = parseFields(paramFields); 160 | 161 | params.embedOptions = parseFields(embedOptionsFields); 162 | 163 | normalizeParams(params); 164 | 165 | return params; 166 | } 167 | 168 | function removeDefaults(params, customDefaults) { 169 | customDefaults = customDefaults || defaults; 170 | 171 | Object.keys(customDefaults).forEach(function(key) { 172 | if (customDefaults.hasOwnProperty(key) && params[key] === customDefaults[key]) { 173 | delete params[key]; 174 | } 175 | }); 176 | 177 | if (params.embedOptions && customDefaults.embedOptions) { 178 | removeDefaults(params.embedOptions, customDefaults.embedOptions); 179 | if (!Object.keys(params.embedOptions).length) { 180 | delete params.embedOptions; 181 | } 182 | } 183 | 184 | return params; 185 | } 186 | 187 | function createEmbed(params) { 188 | var refNode = $('
'); 189 | 190 | embedCount += 1; 191 | 192 | refNode.append([ 193 | '

', 194 | 'Remove', 195 | ' ', 196 | '', 197 | ' ', 198 | '', 199 | '

', 200 | '
', 201 | '
',
202 |           JSON.stringify(params, null, 2),
203 |         '
', 204 | '
', 205 | '
', 206 | '
', 207 | '

Copy the following URL to share this page with this embed!

', 208 | '', 209 | currentPageUrl, '?params=', window.encodeURIComponent(JSON.stringify(params)), 210 | '', 211 | '
', 212 | '
' 213 | ].join('')); 214 | 215 | refNodeRoot.append(refNode); 216 | 217 | params.refNode = refNode.get(0); 218 | 219 | console.log('creating embed with params', params); 220 | 221 | brightcovePlayerLoader(params).then(function(success) { 222 | if (success.type !== brightcovePlayerLoader.EMBED_TYPE_IN_PAGE) { 223 | return; 224 | } 225 | 226 | var player = success.ref; 227 | 228 | window.players[(params.playerId || 'default') + '_' + embedCount] = player; 229 | refNode.find('.btn-remove-player').data({player}); 230 | 231 | // When an in-page player is disposed, we need to clean up its 232 | // surrounding DOM elements. This needs to wait a tick, because 233 | // the player will remove its own DOM element first. 234 | player.on('dispose', function() { 235 | var listGroupItem = $(player.el()).closest('.list-group-item'); 236 | 237 | delete window.players[ + embedCount]; 238 | 239 | window.setTimeout(function() { 240 | if (listGroupItem.length) { 241 | listGroupItem.remove(); 242 | } 243 | }, 1); 244 | }); 245 | 246 | var bc = window.bc; 247 | var major = bc && bc.VERSION ? Number(bc.VERSION.split('.')[0]) : 0; 248 | 249 | if (major < 6) { 250 | $(player.el()).before(''); 251 | } 252 | }, function(err) { 253 | refNode.append(''); 254 | }); 255 | } 256 | 257 | embedCount = 0; 258 | defaults = readParams(); 259 | 260 | delete defaults.accountId; 261 | console.log('defaults', defaults); 262 | 263 | try { 264 | createEmbed(filterParams(JSON.parse(Qs.parse(window.location.search.substring(1)).params))); 265 | } catch (x) {} 266 | 267 | form.on('submit', function(e) { 268 | e.preventDefault(); 269 | createEmbed(removeDefaults(readParams())); 270 | }); 271 | 272 | refNodeRoot.on('click', '.btn-remove-player', function(e) { 273 | var btn = $(this); 274 | 275 | e.preventDefault(); 276 | 277 | // In-page players need to be disposed, iframe players can simply be 278 | // removed from the DOM. 279 | if (btn.data('player')) { 280 | btn.data('player').dispose(); 281 | } else { 282 | btn.closest('.list-group-item').remove(); 283 | } 284 | 285 | // If there are no more players on the page, go ahead and reset the 286 | // global environment. 287 | if (!Object.keys(window.players).length) { 288 | brightcovePlayerLoader.reset(); 289 | console.log('reset global state'); 290 | } 291 | }); 292 | }); 293 | -------------------------------------------------------------------------------- /src/create-embed.js: -------------------------------------------------------------------------------- 1 | import {getDocument} from './utils/environment'; 2 | import urls from './urls'; 3 | 4 | import { 5 | DEFAULT_ASPECT_RATIO, 6 | DEFAULT_IFRAME_HORIZONTAL_PLAYLIST, 7 | DEFAULT_MAX_WIDTH, 8 | EMBED_TAG_NAME_VIDEOJS, 9 | EMBED_TYPE_IFRAME, 10 | REF_NODE_INSERT_PREPEND, 11 | REF_NODE_INSERT_BEFORE, 12 | REF_NODE_INSERT_AFTER, 13 | REF_NODE_INSERT_REPLACE, 14 | JSON_ALLOWED_ATTRS 15 | } from './constants'; 16 | 17 | /** 18 | * Is this value an element? 19 | * 20 | * @param {Element} el 21 | * A maybe element. 22 | * 23 | * @return {boolean} 24 | * Whether or not the value is a element. 25 | */ 26 | const isEl = (el) => Boolean(el && el.nodeType === 1); 27 | 28 | /** 29 | * Is this value an element with a parent node? 30 | * 31 | * @param {Element} el 32 | * A maybe element. 33 | * 34 | * @return {boolean} 35 | * Whether or not the value is a element with a parent node. 36 | */ 37 | const isElInDom = (el) => Boolean(isEl(el) && el.parentNode); 38 | 39 | /** 40 | * Creates an iframe embed code. 41 | * 42 | * @private 43 | * @param {Object} params 44 | * A parameters object. See README for details. 45 | * 46 | * @return {Element} 47 | * The DOM element that will ultimately be passed to the `bc()` function. 48 | */ 49 | const createIframeEmbed = (params) => { 50 | const doc = getDocument(); 51 | const el = doc.createElement('iframe'); 52 | 53 | el.setAttribute('allow', 'autoplay;encrypted-media;fullscreen'); 54 | el.setAttribute('allowfullscreen', 'allowfullscreen'); 55 | el.src = urls.getUrl(params); 56 | 57 | return el; 58 | }; 59 | 60 | /** 61 | * Creates an in-page embed code. 62 | * 63 | * @private 64 | * @param {Object} params 65 | * A parameters object. See README for details. 66 | * 67 | * @return {Element} 68 | * The DOM element that will ultimately be passed to the `bc()` function. 69 | */ 70 | const createInPageEmbed = (params) => { 71 | const {embedOptions} = params; 72 | const doc = getDocument(); 73 | 74 | // We DO NOT include the data-account, data-player, or data-embed attributes 75 | // here because we will be manually initializing the player. 76 | const paramsToAttrs = { 77 | adConfigId: 'data-ad-config-id', 78 | applicationId: 'data-application-id', 79 | catalogSearch: 'data-catalog-search', 80 | catalogSequence: 'data-catalog-sequence', 81 | deliveryConfigId: 'data-delivery-config-id', 82 | playlistId: 'data-playlist-id', 83 | playlistVideoId: 'data-playlist-video-id', 84 | poster: 'poster', 85 | videoId: 'data-video-id' 86 | }; 87 | 88 | const tagName = embedOptions && embedOptions.tagName || EMBED_TAG_NAME_VIDEOJS; 89 | const el = doc.createElement(tagName); 90 | 91 | Object.keys(paramsToAttrs) 92 | .filter(key => params[key]) 93 | .forEach(key => { 94 | let value; 95 | 96 | // If it's not a string, such as with a catalog search or sequence, we 97 | // try to encode it as JSON. 98 | if (typeof params[key] !== 'string' && JSON_ALLOWED_ATTRS.indexOf(key) !== -1) { 99 | try { 100 | value = JSON.stringify(params[key]); 101 | 102 | // If it fails, don't set anything. 103 | } catch (x) { 104 | return; 105 | } 106 | } else { 107 | value = String(params[key]).trim(); 108 | } 109 | 110 | el.setAttribute(paramsToAttrs[key], value); 111 | }); 112 | 113 | el.setAttribute('controls', 'controls'); 114 | el.classList.add('video-js'); 115 | 116 | return el; 117 | }; 118 | 119 | /** 120 | * Wraps an element in responsive intrinsic ratio elements. 121 | * 122 | * @private 123 | * @param {string} embedType 124 | * The type of the embed. 125 | * 126 | * @param {Object} embedOptions 127 | * Embed options from the params. 128 | * 129 | * @param {Element} el 130 | * The DOM element. 131 | * 132 | * @return {Element} 133 | * A new element (if needed). 134 | */ 135 | const wrapResponsive = (embedType, embedOptions, el) => { 136 | if (!embedOptions.responsive) { 137 | return el; 138 | } 139 | 140 | const doc = getDocument(); 141 | 142 | el.style.position = 'absolute'; 143 | el.style.top = '0px'; 144 | el.style.right = '0px'; 145 | el.style.bottom = '0px'; 146 | el.style.left = '0px'; 147 | el.style.width = '100%'; 148 | el.style.height = '100%'; 149 | 150 | const responsive = Object.assign({ 151 | aspectRatio: DEFAULT_ASPECT_RATIO, 152 | iframeHorizontalPlaylist: DEFAULT_IFRAME_HORIZONTAL_PLAYLIST, 153 | maxWidth: DEFAULT_MAX_WIDTH 154 | }, embedOptions.responsive); 155 | 156 | // This value is validate at a higher level, so we can trust that it's in the 157 | // correct format. 158 | const aspectRatio = responsive.aspectRatio.split(':').map(Number); 159 | const inner = doc.createElement('div'); 160 | let paddingTop = (aspectRatio[1] / aspectRatio[0] * 100); 161 | 162 | // For iframes with a horizontal playlist, the playlist takes up 20% of the 163 | // vertical space (if shown); so, adjust the vertical size of the embed to 164 | // avoid black bars. 165 | if (embedType === EMBED_TYPE_IFRAME && responsive.iframeHorizontalPlaylist) { 166 | paddingTop *= 1.25; 167 | } 168 | 169 | inner.style.paddingTop = paddingTop + '%'; 170 | inner.appendChild(el); 171 | 172 | const outer = doc.createElement('div'); 173 | 174 | outer.style.position = 'relative'; 175 | outer.style.display = 'block'; 176 | outer.style.maxWidth = responsive.maxWidth; 177 | outer.appendChild(inner); 178 | 179 | return outer; 180 | }; 181 | 182 | /** 183 | * Wraps an element in a Picture-in-Picture plugin container. 184 | * 185 | * @private 186 | * @param {Object} embedOptions 187 | * Embed options from the params. 188 | * 189 | * @param {Element} el 190 | * The DOM element. 191 | * 192 | * @return {Element} 193 | * A new element (if needed). 194 | */ 195 | const wrapPip = (embedOptions, el) => { 196 | if (!embedOptions.pip) { 197 | return el; 198 | } 199 | 200 | const doc = getDocument(); 201 | const pip = doc.createElement('div'); 202 | 203 | pip.classList.add('vjs-pip-container'); 204 | pip.appendChild(el); 205 | 206 | return pip; 207 | }; 208 | 209 | /** 210 | * Wraps a bare embed element with necessary parent elements, depending on 211 | * embed options given in params. 212 | * 213 | * @private 214 | * @param {string} embedType 215 | * The type of the embed. 216 | * 217 | * @param {Object} embedOptions 218 | * Embed options from the params. 219 | * 220 | * @param {Element} embed 221 | * The embed DOM element. 222 | * 223 | * @return {Element} 224 | * A new element (if needed) or the embed itself. 225 | */ 226 | const wrapEmbed = (embedType, embedOptions, embed) => { 227 | if (!embedOptions) { 228 | return embed; 229 | } 230 | 231 | return wrapPip(embedOptions, wrapResponsive(embedType, embedOptions, embed)); 232 | }; 233 | 234 | /** 235 | * Inserts a previously-created embed element into the page based on params. 236 | * 237 | * @private 238 | * @param {Object} params 239 | * A parameters object. See README for details. 240 | * 241 | * @param {Element} embed 242 | * The embed DOM element. 243 | * 244 | * @return {Element} 245 | * The embed DOM element. 246 | */ 247 | const insertEmbed = (params, embed) => { 248 | const {refNode, refNodeInsert} = params; 249 | const refNodeParent = refNode.parentNode; 250 | 251 | // Wrap the embed, if needed, in container elements to support various 252 | // plugins. 253 | const wrapped = wrapEmbed(params.embedType, params.embedOptions, embed); 254 | 255 | // Decide where to insert the wrapped embed. 256 | if (refNodeInsert === REF_NODE_INSERT_BEFORE) { 257 | refNodeParent.insertBefore(wrapped, refNode); 258 | } else if (refNodeInsert === REF_NODE_INSERT_AFTER) { 259 | refNodeParent.insertBefore(wrapped, refNode.nextElementSibling || null); 260 | } else if (refNodeInsert === REF_NODE_INSERT_REPLACE) { 261 | refNodeParent.replaceChild(wrapped, refNode); 262 | } else if (refNodeInsert === REF_NODE_INSERT_PREPEND) { 263 | refNode.insertBefore(wrapped, refNode.firstChild || null); 264 | 265 | // Append is the default. 266 | } else { 267 | refNode.appendChild(wrapped); 268 | } 269 | 270 | // If the playlist embed option is provided, we need to add a playlist element 271 | // immediately after the embed. This has to happen after the embed is inserted 272 | // into the DOM (above). 273 | if (params.embedOptions && params.embedOptions.playlist) { 274 | const doc = getDocument(); 275 | const playlistTagName = params.embedOptions.playlist.legacy ? 'ul' : 'div'; 276 | const playlist = doc.createElement(playlistTagName); 277 | 278 | playlist.classList.add('vjs-playlist'); 279 | embed.parentNode.insertBefore(playlist, embed.nextElementSibling || null); 280 | } 281 | 282 | // Clean up internal reference to the refNode to avoid potential memory 283 | // leaks in case the params get persisted somewhere. We won't need it beyond 284 | // this point. 285 | params.refNode = null; 286 | 287 | // Return the original embed element that can be passed to `bc()`. 288 | return embed; 289 | }; 290 | 291 | /** 292 | * Handles `onEmbedCreated` callback invocation. 293 | * 294 | * @private 295 | * @param {Object} params 296 | * A parameters object. See README for details. 297 | * 298 | * @param {Element} embed 299 | * The embed DOM element. 300 | * 301 | * @return {Element} 302 | * A possibly-new DOM element. 303 | */ 304 | const onEmbedCreated = (params, embed) => { 305 | if (typeof params.onEmbedCreated !== 'function') { 306 | return embed; 307 | } 308 | 309 | const result = params.onEmbedCreated(embed); 310 | 311 | if (isEl(result)) { 312 | return result; 313 | } 314 | 315 | return embed; 316 | }; 317 | 318 | /** 319 | * Creates an embed code of the appropriate type, runs any customizations 320 | * necessary, and inserts it into the DOM. 321 | * 322 | * @param {Object} params 323 | * A parameters object. See README for details. 324 | * 325 | * @return {Element} 326 | * The DOM element that will ultimately be passed to the `bc()` 327 | * function. Even when customized or wrapped, the return value will be 328 | * the target element. 329 | */ 330 | const createEmbed = (params) => { 331 | const embed = (params.embedType === EMBED_TYPE_IFRAME) ? 332 | createIframeEmbed(params) : 333 | createInPageEmbed(params); 334 | 335 | return insertEmbed(params, onEmbedCreated(params, embed)); 336 | }; 337 | 338 | export default createEmbed; 339 | export {isEl, isElInDom}; 340 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {getDocument, getWindow} from './utils/environment'; 2 | import {version as VERSION} from '../package.json'; 3 | import createEmbed from './create-embed'; 4 | import {isElInDom} from './create-embed'; 5 | import env from './env'; 6 | import playerScriptCache from './player-script-cache'; 7 | import urls from './urls'; 8 | 9 | import { 10 | DEFAULTS, 11 | EMBED_TAG_NAME_VIDEO, 12 | EMBED_TAG_NAME_VIDEOJS, 13 | EMBED_TYPE_IN_PAGE, 14 | EMBED_TYPE_IFRAME, 15 | REF_NODE_INSERT_APPEND, 16 | REF_NODE_INSERT_PREPEND, 17 | REF_NODE_INSERT_BEFORE, 18 | REF_NODE_INSERT_AFTER, 19 | REF_NODE_INSERT_REPLACE 20 | } from './constants'; 21 | 22 | // Look through the page for any pre-existing players. 23 | const window = getWindow(); 24 | 25 | env.detectPlayers(); 26 | 27 | /** 28 | * Is this value a function? 29 | * 30 | * @private 31 | * @param {Function} fn 32 | * A maybe function. 33 | * 34 | * @return {boolean} 35 | * Whether or not the value is a function. 36 | */ 37 | const isFn = (fn) => typeof fn === 'function'; 38 | 39 | /** 40 | * Checks whether an embedType parameter is valid. 41 | * 42 | * @private 43 | * @param {string} embedType 44 | * The value to test. 45 | * 46 | * @return {boolean} 47 | * Whether the value is valid. 48 | */ 49 | const isValidEmbedType = (embedType) => 50 | embedType === EMBED_TYPE_IN_PAGE || 51 | embedType === EMBED_TYPE_IFRAME; 52 | 53 | /** 54 | * Checks whether an embedOptions.tagName parameter is valid. 55 | * 56 | * @private 57 | * @param {string} tagName 58 | * The value to test. 59 | * 60 | * @return {boolean} 61 | * Whether the value is valid. 62 | */ 63 | const isValidTagName = (tagName) => 64 | tagName === EMBED_TAG_NAME_VIDEOJS || 65 | tagName === EMBED_TAG_NAME_VIDEO; 66 | 67 | /** 68 | * Checks whether a refNodeInsert parameter is valid. 69 | * 70 | * @private 71 | * @param {string} refNodeInsert 72 | * The value to test. 73 | * 74 | * @return {boolean} 75 | * Whether the value is valid. 76 | */ 77 | const isValidRootInsert = (refNodeInsert) => 78 | refNodeInsert === REF_NODE_INSERT_APPEND || 79 | refNodeInsert === REF_NODE_INSERT_PREPEND || 80 | refNodeInsert === REF_NODE_INSERT_BEFORE || 81 | refNodeInsert === REF_NODE_INSERT_AFTER || 82 | refNodeInsert === REF_NODE_INSERT_REPLACE; 83 | 84 | /** 85 | * Checks parameters and throws an error on validation problems. 86 | * 87 | * @private 88 | * @param {Object} params 89 | * A parameters object. See README for details. 90 | * 91 | * @throws {Error} If accountId is missing. 92 | * @throws {Error} If refNode is missing or invalid. 93 | * @throws {Error} If embedType is missing or invalid. 94 | * @throws {Error} If attempting to use an iframe embed with options. 95 | * @throws {Error} If attempting to use embedOptions.responsiveIframe with a 96 | * non-iframe embed. 97 | * @throws {Error} If refNodeInsert is missing or invalid. 98 | */ 99 | const checkParams = (params) => { 100 | const { 101 | accountId, 102 | embedOptions, 103 | embedType, 104 | options, 105 | refNode, 106 | refNodeInsert 107 | } = params; 108 | 109 | if (!accountId) { 110 | throw new Error('accountId is required'); 111 | 112 | } else if (!isElInDom(refNode)) { 113 | throw new Error('refNode must resolve to a node attached to the DOM'); 114 | 115 | } else if (!isValidEmbedType(embedType)) { 116 | throw new Error('embedType is missing or invalid'); 117 | 118 | } else if (embedType === EMBED_TYPE_IFRAME && options) { 119 | throw new Error('cannot use options with an iframe embed'); 120 | 121 | } else if (embedOptions && embedOptions.tagName !== undefined && !isValidTagName(embedOptions.tagName)) { 122 | throw new Error(`embedOptions.tagName is invalid (value: "${embedOptions.tagName}")`); 123 | 124 | } else if (embedOptions && 125 | embedOptions.responsive && 126 | embedOptions.responsive.aspectRatio && 127 | !(/^\d+\:\d+$/).test(embedOptions.responsive.aspectRatio)) { 128 | throw new Error(`embedOptions.responsive.aspectRatio must be in the "n:n" format (value: "${embedOptions.responsive.aspectRatio}")`); 129 | 130 | } else if (!isValidRootInsert(refNodeInsert)) { 131 | throw new Error('refNodeInsert is missing or invalid'); 132 | } 133 | }; 134 | 135 | /** 136 | * Normalizes a `refNode` param to an element - or `null`. 137 | * 138 | * @private 139 | * @param {Element|string} refNode 140 | * The value of a `refNode` param. 141 | * 142 | * @return {Element|null} 143 | * A DOM element or `null` if the `refNode` was given as a string and 144 | * did not match an element. 145 | */ 146 | const resolveRefNode = (refNode) => { 147 | if (isElInDom(refNode)) { 148 | return refNode; 149 | } 150 | 151 | if (typeof refNode === 'string') { 152 | const doc = getDocument(); 153 | 154 | return doc.querySelector(refNode); 155 | } 156 | 157 | return null; 158 | }; 159 | 160 | /** 161 | * Initializes a player and returns it. 162 | * 163 | * @private 164 | * @param {Object} params 165 | * A parameters object. See README for details. 166 | * 167 | * @param {Element} embed 168 | * An element that will be passed to the `bc()` function. 169 | * 170 | * @param {Function} resolve 171 | * A function to call if a player is successfully initialized. 172 | * 173 | * @param {Function} reject 174 | * A function to call if a player fails to be initialized. 175 | * 176 | * @return {Object} 177 | * A success object whose `ref` is a player. 178 | */ 179 | const initPlayer = (params, embed, resolve, reject) => { 180 | const {embedId, playerId} = params; 181 | const bc = window.bc[`${playerId}_${embedId}`] || window.bc; 182 | 183 | if (!bc) { 184 | return reject(new Error(`missing bc function for ${playerId}`)); 185 | } 186 | 187 | playerScriptCache.store(params); 188 | 189 | let player; 190 | 191 | try { 192 | player = bc(embed, params.options); 193 | 194 | // Add a PLAYER_LOADER property to bcinfo to indicate this player was 195 | // loaded via that mechanism. 196 | if (player.bcinfo) { 197 | player.bcinfo.PLAYER_LOADER = true; 198 | } 199 | } catch (x) { 200 | let message = 'Could not initialize the Brightcove Player.'; 201 | 202 | // Update the rejection message based on known conditions that can cause it. 203 | if (params.embedOptions.tagName === EMBED_TAG_NAME_VIDEOJS) { 204 | message += ' You are attempting to embed using a "video-js" element.' + 205 | ' Please ensure that your Player is v6.11.0 or newer in order to' + 206 | ' support this embed type. Alternatively, pass `"video"` for' + 207 | ' `embedOptions.tagName`.'; 208 | } 209 | 210 | return reject(new Error(message)); 211 | } 212 | 213 | resolve({ 214 | type: EMBED_TYPE_IN_PAGE, 215 | ref: player 216 | }); 217 | }; 218 | 219 | /** 220 | * Loads a player from CDN and embeds it. 221 | * 222 | * @private 223 | * @param {Object} params 224 | * A parameters object. See README for details. 225 | * 226 | * @param {Function} resolve 227 | * A function to call if a player is successfully initialized. 228 | * 229 | * @param {Function} reject 230 | * A function to call if a player fails to be initialized. 231 | * @return {void} 232 | */ 233 | const loadPlayer = (params, resolve, reject) => { 234 | params.refNode = resolveRefNode(params.refNode); 235 | 236 | checkParams(params); 237 | 238 | const {refNode, refNodeInsert} = params; 239 | 240 | // Store a reference to the refNode parent. When we use the replace method, 241 | // we'll need it as the location to store the script element. 242 | const refNodeParent = refNode.parentNode; 243 | const embed = createEmbed(params); 244 | 245 | // If this is an iframe, all we need to do is create the embed code and 246 | // inject it. Because there is no reliable way to hook into an iframe from 247 | // the parent page, we simply resolve immediately upon creating the embed. 248 | if (params.embedType === EMBED_TYPE_IFRAME) { 249 | resolve({ 250 | type: EMBED_TYPE_IFRAME, 251 | ref: embed 252 | }); 253 | return; 254 | } 255 | 256 | // If we've already downloaded this script or detected a matching global, we 257 | // should have the proper `bc` global and can bypass the script creation 258 | // process. 259 | if (playerScriptCache.has(params)) { 260 | return initPlayer(params, embed, resolve, reject); 261 | } 262 | 263 | const doc = getDocument(); 264 | const script = doc.createElement('script'); 265 | 266 | script.onload = () => initPlayer(params, embed, resolve, reject); 267 | 268 | script.onerror = () => { 269 | reject(new Error('player script could not be downloaded')); 270 | }; 271 | 272 | script.async = true; 273 | script.charset = 'utf-8'; 274 | script.src = urls.getUrl(params); 275 | 276 | if (refNodeInsert === REF_NODE_INSERT_REPLACE) { 277 | refNodeParent.appendChild(script); 278 | } else { 279 | refNode.appendChild(script); 280 | } 281 | }; 282 | 283 | /** 284 | * A function for asynchronously loading a Brightcove Player into a web page. 285 | * 286 | * @param {Object} parameters 287 | * A parameters object. See README for details. 288 | * 289 | * @return {Promise|undefined} 290 | * A Promise, if possible. 291 | */ 292 | const brightcovePlayerLoader = (parameters) => { 293 | const params = Object.assign({}, DEFAULTS, parameters); 294 | const {Promise, onSuccess, onFailure} = params; 295 | 296 | // When Promise is not available or any success/failure callback is given, 297 | // do not attempt to use Promises. 298 | if (!isFn(Promise) || isFn(onSuccess) || isFn(onFailure)) { 299 | return loadPlayer( 300 | params, 301 | isFn(onSuccess) ? onSuccess : () => {}, 302 | isFn(onFailure) ? onFailure : (err) => { 303 | throw err; 304 | } 305 | ); 306 | } 307 | 308 | // Promises are supported, use 'em. 309 | return new Promise((resolve, reject) => loadPlayer(params, resolve, reject)); 310 | }; 311 | 312 | /** 313 | * Expose a non-writable, non-configurable property on the 314 | * `brightcovePlayerLoader` function. 315 | * 316 | * @private 317 | * @param {string} key 318 | * The property key. 319 | * 320 | * @param {string|Function} value 321 | * The value. 322 | */ 323 | const expose = (key, value) => { 324 | Object.defineProperty(brightcovePlayerLoader, key, { 325 | configurable: false, 326 | enumerable: true, 327 | value, 328 | writable: false 329 | }); 330 | }; 331 | 332 | /** 333 | * Get the base URL for players. By default, this will be the Brightcove CDN. 334 | * 335 | * @return {string} 336 | * The current base URL. 337 | */ 338 | expose('getBaseUrl', () => urls.getBaseUrl()); 339 | 340 | /** 341 | * Set the base URL for players. By default, this will be the Brightcove CDN, 342 | * but can be overridden with this function. 343 | * 344 | * @param {string} baseUrl 345 | * A new base URL (instead of Brightcove CDN). 346 | */ 347 | expose('setBaseUrl', (baseUrl) => { 348 | urls.setBaseUrl(baseUrl); 349 | }); 350 | 351 | /** 352 | * Get the URL for a player. 353 | */ 354 | expose('getUrl', (options) => urls.getUrl(options)); 355 | 356 | /** 357 | * Completely resets global state. 358 | * 359 | * This will dispose ALL Video.js players on the page and remove ALL `bc` and 360 | * `videojs` globals it finds. 361 | */ 362 | expose('reset', () => env.reset()); 363 | 364 | // Define some read-only constants on the exported function. 365 | [ 366 | ['EMBED_TAG_NAME_VIDEO', EMBED_TAG_NAME_VIDEO], 367 | ['EMBED_TAG_NAME_VIDEOJS', EMBED_TAG_NAME_VIDEOJS], 368 | ['EMBED_TYPE_IN_PAGE', EMBED_TYPE_IN_PAGE], 369 | ['EMBED_TYPE_IFRAME', EMBED_TYPE_IFRAME], 370 | ['REF_NODE_INSERT_APPEND', REF_NODE_INSERT_APPEND], 371 | ['REF_NODE_INSERT_PREPEND', REF_NODE_INSERT_PREPEND], 372 | ['REF_NODE_INSERT_BEFORE', REF_NODE_INSERT_BEFORE], 373 | ['REF_NODE_INSERT_AFTER', REF_NODE_INSERT_AFTER], 374 | ['REF_NODE_INSERT_REPLACE', REF_NODE_INSERT_REPLACE], 375 | ['VERSION', VERSION] 376 | ].forEach(arr => { 377 | expose(arr[0], arr[1]); 378 | }); 379 | 380 | export default brightcovePlayerLoader; 381 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import {getDocument, getWindow} from '../src/utils/environment'; 2 | import QUnit from 'qunit'; 3 | import brightcovePlayerLoader from '../src/'; 4 | import urls from '../src/urls'; 5 | 6 | const document = getDocument(); 7 | const window = getWindow(); 8 | 9 | QUnit.test('the environment is sane', function(assert) { 10 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); 11 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); 12 | assert.strictEqual( 13 | typeof brightcovePlayerLoader, 14 | 'function', 15 | 'brightcovePlayerLoader is a function' 16 | ); 17 | }); 18 | 19 | QUnit.module('brightcove-player-loader', function(hooks) { 20 | const originalBaseUrl = urls.getBaseUrl(); 21 | 22 | hooks.before(function() { 23 | urls.setBaseUrl(`${window.location.origin}/vendor/`); 24 | }); 25 | 26 | hooks.beforeEach(function() { 27 | this.fixture = document.getElementById('qunit-fixture'); 28 | }); 29 | 30 | hooks.afterEach(function() { 31 | brightcovePlayerLoader.reset(); 32 | }); 33 | 34 | hooks.after(function() { 35 | urls.setBaseUrl(originalBaseUrl); 36 | }); 37 | 38 | QUnit.test('exposes several constant values', function(assert) { 39 | [ 40 | 'EMBED_TAG_NAME_VIDEO', 41 | 'EMBED_TAG_NAME_VIDEOJS', 42 | 'EMBED_TYPE_IN_PAGE', 43 | 'EMBED_TYPE_IFRAME', 44 | 'REF_NODE_INSERT_APPEND', 45 | 'REF_NODE_INSERT_PREPEND', 46 | 'REF_NODE_INSERT_BEFORE', 47 | 'REF_NODE_INSERT_AFTER', 48 | 'REF_NODE_INSERT_REPLACE', 49 | 'VERSION' 50 | ].forEach(k => { 51 | assert.ok(brightcovePlayerLoader.hasOwnProperty(k), `${k} exists`); 52 | }); 53 | }); 54 | 55 | QUnit.test('exposes several methods', function(assert) { 56 | [ 57 | 'getBaseUrl', 58 | 'setBaseUrl', 59 | 'reset' 60 | ].forEach(k => { 61 | assert.strictEqual(typeof brightcovePlayerLoader[k], 'function', `${k} is a function`); 62 | }); 63 | }); 64 | 65 | QUnit.test('default/minimal usage', function(assert) { 66 | const done = assert.async(); 67 | 68 | assert.expect(2); 69 | 70 | brightcovePlayerLoader({ 71 | accountId: '1', 72 | refNode: this.fixture, 73 | onEmbedCreated(embed) { 74 | embed.id = 'derp'; 75 | } 76 | }) 77 | .then(success => { 78 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IN_PAGE, 'the expected embed type was passed through the Promise'); 79 | assert.strictEqual(success.ref, window.videojs.players.derp, 'the expected player was passed through the Promise'); 80 | done(); 81 | }) 82 | .catch(done); 83 | }); 84 | 85 | QUnit.test('usage inpage & playerUrl', function(assert) { 86 | const done = assert.async(); 87 | 88 | assert.expect(2); 89 | 90 | brightcovePlayerLoader({ 91 | accountId: '1', 92 | refNode: this.fixture, 93 | playerUrl: `${window.location.origin}/vendor/1/default_default/index.min.js`, 94 | onEmbedCreated(embed) { 95 | embed.id = 'derp'; 96 | } 97 | }).then(success => { 98 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IN_PAGE, 'the expected embed type was passed through the Promise'); 99 | assert.strictEqual(success.ref, window.videojs.players.derp, 'the expected player was passed through the Promise'); 100 | done(); 101 | }).catch(done); 102 | }); 103 | 104 | QUnit.test('usage iframe & playerUrl', function(assert) { 105 | const done = assert.async(); 106 | 107 | assert.expect(3); 108 | 109 | brightcovePlayerLoader({ 110 | accountId: '1', 111 | refNode: this.fixture, 112 | embedType: brightcovePlayerLoader.EMBED_TYPE_IFRAME, 113 | playerUrl: `${window.location.origin}/vendor/1/default_default/index.html`, 114 | onEmbedCreated(embed) { 115 | embed.id = 'derp'; 116 | } 117 | }).then(success => { 118 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IFRAME, 'the expected embed type was passed through the Promise'); 119 | assert.strictEqual(success.ref.nodeType, 1, 'it is a DOM node'); 120 | assert.strictEqual(success.ref.parentNode, this.fixture, 'it is in the DOM where we expect it'); 121 | done(); 122 | }).catch(done); 123 | }); 124 | 125 | QUnit.test('default/minimal usage - with refNode as string', function(assert) { 126 | const done = assert.async(); 127 | 128 | assert.expect(2); 129 | 130 | brightcovePlayerLoader({ 131 | accountId: '1', 132 | refNode: '#qunit-fixture', 133 | onEmbedCreated(embed) { 134 | embed.id = 'derp'; 135 | } 136 | }) 137 | .then(success => { 138 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IN_PAGE, 'the expected embed type was passed through the Promise'); 139 | assert.strictEqual(success.ref, window.videojs.players.derp, 'the expected player was passed through the Promise'); 140 | done(); 141 | }) 142 | .catch(done); 143 | }); 144 | 145 | QUnit.test('default/minimal usage - with refNodeInsert as "replace"', function(assert) { 146 | const done = assert.async(); 147 | 148 | assert.expect(2); 149 | 150 | brightcovePlayerLoader({ 151 | accountId: '1', 152 | refNode: '#qunit-fixture', 153 | onEmbedCreated(embed) { 154 | embed.id = 'derp'; 155 | } 156 | }) 157 | .then(success => { 158 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IN_PAGE, 'the expected embed type was passed through the Promise'); 159 | assert.strictEqual(success.ref, window.videojs.players.derp, 'the expected player was passed through the Promise'); 160 | done(); 161 | }) 162 | .catch(done); 163 | }); 164 | 165 | QUnit.test('default/minimal usage - with callbacks instead of Promises', function(assert) { 166 | const done = assert.async(); 167 | 168 | assert.expect(3); 169 | 170 | const result = brightcovePlayerLoader({ 171 | accountId: '1', 172 | refNode: this.fixture, 173 | onEmbedCreated(embed) { 174 | embed.id = 'derp'; 175 | }, 176 | onFailure: () => { 177 | done(); 178 | }, 179 | onSuccess: (success) => { 180 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IN_PAGE, 'the expected embed type was passed through the Promise'); 181 | assert.strictEqual(success.ref, window.videojs.players.derp, 'the expected player was passed through the Promise'); 182 | assert.strictEqual(result, undefined, 'no Promise was returned'); 183 | done(); 184 | } 185 | }); 186 | }); 187 | 188 | QUnit.test('iframes resolve with a DOM element', function(assert) { 189 | const done = assert.async(); 190 | 191 | assert.expect(3); 192 | 193 | brightcovePlayerLoader({ 194 | accountId: '1', 195 | embedType: brightcovePlayerLoader.EMBED_TYPE_IFRAME, 196 | refNode: this.fixture, 197 | onEmbedCreated(embed) { 198 | embed.id = 'derp'; 199 | } 200 | }) 201 | .then(success => { 202 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IFRAME, 'the expected embed type was passed through the Promise'); 203 | assert.strictEqual(success.ref.nodeType, 1, 'it is a DOM node'); 204 | assert.strictEqual(success.ref.parentNode, this.fixture, 'it is in the DOM where we expect it'); 205 | done(); 206 | }) 207 | .catch(done); 208 | }); 209 | 210 | QUnit.test('iframes resolve with a DOM element - with callbacks instead of Promises', function(assert) { 211 | const done = assert.async(); 212 | 213 | assert.expect(4); 214 | 215 | const result = brightcovePlayerLoader({ 216 | accountId: '1', 217 | embedType: brightcovePlayerLoader.EMBED_TYPE_IFRAME, 218 | refNode: this.fixture, 219 | onEmbedCreated(embed) { 220 | embed.id = 'derp'; 221 | }, 222 | onFailure: () => { 223 | done(); 224 | }, 225 | onSuccess: (success) => { 226 | assert.strictEqual(success.type, brightcovePlayerLoader.EMBED_TYPE_IFRAME, 'the expected embed type was passed through the Promise'); 227 | assert.strictEqual(success.ref.nodeType, 1, 'it is a DOM node'); 228 | assert.strictEqual(success.ref.parentNode, this.fixture, 'it is in the DOM where we expect it'); 229 | assert.strictEqual(result, undefined, 'no Promise was returned'); 230 | done(); 231 | } 232 | }); 233 | }); 234 | 235 | QUnit.test('does not re-download scripts it already has', function(assert) { 236 | const done = assert.async(); 237 | 238 | assert.expect(2); 239 | 240 | brightcovePlayerLoader({ 241 | accountId: '1', 242 | refNode: this.fixture 243 | }) 244 | 245 | // When the first player download is completed, immediately embed the 246 | // same player again. 247 | .then(success => brightcovePlayerLoader({ 248 | accountId: '1', 249 | refNode: this.fixture 250 | })) 251 | .then(success => { 252 | assert.strictEqual(this.fixture.querySelectorAll('script').length, 1, 'only one script was created'); 253 | assert.strictEqual(this.fixture.querySelectorAll('.video-js').length, 2, 'but there are two players'); 254 | done(); 255 | }) 256 | .catch(done); 257 | }); 258 | 259 | QUnit.test('brightcovePlayerLoader.reset', function(assert) { 260 | const done = assert.async(); 261 | let firstPlayer; 262 | 263 | assert.expect(14); 264 | 265 | brightcovePlayerLoader({ 266 | accountId: '1', 267 | refNode: this.fixture 268 | }) 269 | .then(success => { 270 | firstPlayer = success.ref; 271 | 272 | return brightcovePlayerLoader({ 273 | accountId: '1', 274 | playerId: '2', 275 | embedId: '3', 276 | refNode: this.fixture 277 | }); 278 | }) 279 | .then(success => { 280 | const a = firstPlayer; 281 | const b = success.ref; 282 | 283 | firstPlayer = null; 284 | 285 | assert.ok(window.bc, 'bc exists'); 286 | assert.ok(window.videojs, 'videojs exists'); 287 | assert.ok(window.bc.videojs, 'bc.videojs exists'); 288 | assert.ok(window.bc.default_default, 'bc.default_default exists'); 289 | assert.ok(window.bc.default_default.videojs, 'bc.default_default.videojs exists'); 290 | assert.ok(window.bc['2_3'], 'bc.2_3 exists'); 291 | assert.ok(window.bc['2_3'].videojs, 'bc.2_3.videojs exists'); 292 | 293 | assert.strictEqual(a.el().parentNode, this.fixture, 'player A is in the DOM'); 294 | assert.strictEqual(b.el().parentNode, this.fixture, 'player B is in the DOM'); 295 | 296 | brightcovePlayerLoader.reset(); 297 | 298 | assert.notOk(window.bc, 'bc is gone'); 299 | assert.notOk(window.videojs, 'videojs is gone'); 300 | 301 | assert.strictEqual(a.el(), null, 'player A is disposed'); 302 | assert.strictEqual(b.el(), null, 'player B is disposed'); 303 | assert.notOk(this.fixture.hasChildNodes(), 'no more players or scripts in the fixture'); 304 | 305 | done(); 306 | }) 307 | .catch(done); 308 | }); 309 | 310 | QUnit.module('error states'); 311 | 312 | QUnit.test('accountId is required', function(assert) { 313 | assert.rejects(brightcovePlayerLoader(), new Error('accountId is required')); 314 | }); 315 | 316 | QUnit.test('refNode must resolve to a node attached to the DOM', function(assert) { 317 | assert.rejects(brightcovePlayerLoader({ 318 | accountId: '1' 319 | }), new Error('refNode must resolve to a node attached to the DOM')); 320 | 321 | assert.rejects(brightcovePlayerLoader({ 322 | accountId: '1', 323 | refNode: true 324 | }), new Error('refNode must resolve to a node attached to the DOM')); 325 | 326 | assert.rejects(brightcovePlayerLoader({ 327 | accountId: '1', 328 | refNode: document.createElement('div') 329 | }), new Error('refNode must resolve to a node attached to the DOM')); 330 | }); 331 | 332 | QUnit.test('embedType is missing or invalid', function(assert) { 333 | assert.rejects(brightcovePlayerLoader({ 334 | accountId: '1', 335 | refNode: this.fixture, 336 | embedType: '' 337 | }), new Error('embedType is missing or invalid')); 338 | 339 | assert.rejects(brightcovePlayerLoader({ 340 | accountId: '1', 341 | refNode: this.fixture, 342 | embedType: 'asdf' 343 | }), new Error('embedType is missing or invalid')); 344 | 345 | assert.rejects(brightcovePlayerLoader({ 346 | accountId: '1', 347 | refNode: this.fixture, 348 | embedType: null 349 | }), new Error('embedType is missing or invalid')); 350 | }); 351 | 352 | QUnit.test('cannot use options with an iframe embed', function(assert) { 353 | assert.rejects(brightcovePlayerLoader({ 354 | accountId: '1', 355 | refNode: this.fixture, 356 | embedType: brightcovePlayerLoader.EMBED_TYPE_IFRAME, 357 | options: {} 358 | }), new Error('cannot use options with an iframe embed')); 359 | }); 360 | 361 | QUnit.test('embedOptions.tagName is invalid', function(assert) { 362 | assert.rejects(brightcovePlayerLoader({ 363 | accountId: '1', 364 | refNode: this.fixture, 365 | embedOptions: { 366 | tagName: 'doh' 367 | } 368 | }), new Error('embedOptions.tagName is invalid (value: "doh")')); 369 | }); 370 | 371 | QUnit.test('embedOptions.responsive.aspectRatio must be in the "n:n" format', function(assert) { 372 | assert.rejects(brightcovePlayerLoader({ 373 | accountId: '1', 374 | refNode: this.fixture, 375 | embedOptions: { 376 | responsive: { 377 | aspectRatio: 'asdf' 378 | } 379 | } 380 | }), new Error('embedOptions.responsive.aspectRatio must be in the "n:n" format (value: "asdf")')); 381 | }); 382 | 383 | QUnit.test('refNodeInsert is missing or invalid', function(assert) { 384 | assert.rejects(brightcovePlayerLoader({ 385 | accountId: '1', 386 | refNode: this.fixture, 387 | refNodeInsert: '' 388 | }), new Error('refNodeInsert is missing or invalid')); 389 | 390 | assert.rejects(brightcovePlayerLoader({ 391 | accountId: '1', 392 | refNode: this.fixture, 393 | refNodeInsert: 'asdf' 394 | }), new Error('refNodeInsert is missing or invalid')); 395 | 396 | assert.rejects(brightcovePlayerLoader({ 397 | accountId: '1', 398 | refNode: this.fixture, 399 | refNodeInsert: null 400 | }), new Error('refNodeInsert is missing or invalid')); 401 | }); 402 | }); 403 | -------------------------------------------------------------------------------- /test/create-embed.test.js: -------------------------------------------------------------------------------- 1 | import {getDocument} from '../src/utils/environment'; 2 | import QUnit from 'qunit'; 3 | import createEmbed from '../src/create-embed'; 4 | 5 | const document = getDocument(); 6 | 7 | QUnit.module('create-embed', function(hooks) { 8 | 9 | hooks.beforeEach(function() { 10 | this.fixture = document.getElementById('qunit-fixture'); 11 | }); 12 | 13 | hooks.afterEach(function() { 14 | this.fixture = null; 15 | }); 16 | 17 | QUnit.module('in-page'); 18 | 19 | QUnit.test('creates an in-page embed by default', function(assert) { 20 | const embed = createEmbed({ 21 | refNode: this.fixture, 22 | refNodeInsert: 'append' 23 | }); 24 | 25 | assert.strictEqual(embed.nodeName, 'VIDEO-JS', 'created an in-page embed'); 26 | assert.strictEqual(embed.parentNode, this.fixture, 'appended it to the fixture'); 27 | assert.ok(embed.hasAttribute('controls'), 'has controls attribute'); 28 | assert.ok(embed.classList.contains('video-js'), 'has video-js class'); 29 | assert.notOk(embed.hasAttribute('data-account'), 'we never include data-account because we want to init players ourselves'); 30 | assert.notOk(embed.hasAttribute('data-player'), 'we never include data-player because we want to init players ourselves'); 31 | assert.notOk(embed.hasAttribute('data-embed'), 'we never include data-embed because we want to init players ourselves'); 32 | }); 33 | 34 | QUnit.test('populates certain attributes from params', function(assert) { 35 | const embed = createEmbed({ 36 | refNode: this.fixture, 37 | refNodeInsert: 'append', 38 | adConfigId: 'ad-conf-id', 39 | applicationId: 'app-id', 40 | catalogSearch: 'cat-search', 41 | catalogSequence: 'cat-seq', 42 | deliveryConfigId: 'conf-id', 43 | playlistId: 'pl-id', 44 | playlistVideoId: 'pl-v-id', 45 | poster: 'pstr', 46 | videoId: 'v-id' 47 | }); 48 | 49 | assert.strictEqual(embed.getAttribute('data-ad-config-id'), 'ad-conf-id', 'has correct data-ad-config-id attribute'); 50 | assert.strictEqual(embed.getAttribute('data-application-id'), 'app-id', 'has correct data-application-id attribute'); 51 | assert.strictEqual(embed.getAttribute('data-catalog-search'), 'cat-search', 'has correct data-catalog-search attribute'); 52 | assert.strictEqual(embed.getAttribute('data-catalog-sequence'), 'cat-seq', 'has correct data-catalog-sequence attribute'); 53 | assert.strictEqual(embed.getAttribute('data-delivery-config-id'), 'conf-id', 'has correct data-delivery-config-id attribute'); 54 | assert.strictEqual(embed.getAttribute('data-playlist-id'), 'pl-id', 'has correct data-playlist-id attribute'); 55 | assert.strictEqual(embed.getAttribute('data-playlist-video-id'), 'pl-v-id', 'has correct data-playlist-video-id attribute'); 56 | assert.strictEqual(embed.getAttribute('poster'), 'pstr', 'has correct data-playlist-video-id attribute'); 57 | assert.strictEqual(embed.getAttribute('data-video-id'), 'v-id', 'has correct data-video-id attribute'); 58 | }); 59 | 60 | QUnit.test('JSON-encodes certain attributes', function(assert) { 61 | const embed = createEmbed({ 62 | refNode: this.fixture, 63 | refNodeInsert: 'append', 64 | catalogSearch: {q: 'cat-search'}, 65 | catalogSequence: [{q: 'cat-seq-1'}, {q: 'cat-seq-2'}] 66 | }); 67 | 68 | assert.strictEqual(embed.getAttribute('data-catalog-search'), '{"q":"cat-search"}', 'has correct data-catalog-search attribute'); 69 | assert.strictEqual(embed.getAttribute('data-catalog-sequence'), '[{"q":"cat-seq-1"},{"q":"cat-seq-2"}]', 'has correct data-catalog-sequence attribute'); 70 | }); 71 | 72 | QUnit.module('iframe'); 73 | 74 | QUnit.test('create an iframe embed', function(assert) { 75 | const embed = createEmbed({ 76 | embedType: 'iframe', 77 | refNode: this.fixture, 78 | refNodeInsert: 'append' 79 | }); 80 | 81 | assert.strictEqual(embed.nodeName, 'IFRAME', 'created an iframe embed'); 82 | assert.strictEqual(embed.parentNode, this.fixture, 'appended it to the fixture'); 83 | assert.strictEqual(embed.getAttribute('allow'), 'autoplay;encrypted-media;fullscreen', 'has correct allow attribute'); 84 | assert.ok(embed.hasAttribute('allowfullscreen'), 'has allowfullscreen attribute'); 85 | }); 86 | 87 | QUnit.test('populates certain query string parameters from params', function(assert) { 88 | const embed = createEmbed({ 89 | embedType: 'iframe', 90 | refNode: this.fixture, 91 | refNodeInsert: 'append', 92 | applicationId: 'app-id', 93 | catalogSearch: 'cat-search', 94 | catalogSequence: 'cat-seq', 95 | playlistId: 'pl-id', 96 | playlistVideoId: 'pl-v-id', 97 | videoId: 'v-id' 98 | }); 99 | 100 | const src = embed.getAttribute('src'); 101 | 102 | assert.ok(src.indexOf('applicationId=app-id') > -1, 'has correct applicationId param'); 103 | assert.ok(src.indexOf('catalogSearch=cat-search') > -1, 'has correct catalogSearch param'); 104 | assert.ok(src.indexOf('catalogSequence=cat-seq') > -1, 'has correct catalogSequence param'); 105 | assert.ok(src.indexOf('playlistId=pl-id') > -1, 'has correct playlistId param'); 106 | assert.ok(src.indexOf('playlistVideoId=pl-v-id') > -1, 'has correct playlistVideoId param'); 107 | assert.ok(src.indexOf('videoId=v-id') > -1, 'has correct videoId param'); 108 | }); 109 | 110 | QUnit.test('JSON-encodes certain query string parameters', function(assert) { 111 | const embed = createEmbed({ 112 | embedType: 'iframe', 113 | refNode: this.fixture, 114 | refNodeInsert: 'append', 115 | catalogSearch: {q: 'cat-search'}, 116 | catalogSequence: [{q: 'cat-seq-1'}, {q: 'cat-seq-2'}] 117 | }); 118 | 119 | const src = embed.getAttribute('src'); 120 | 121 | assert.ok(src.indexOf('catalogSearch=%7B%22q%22%3A%22cat-search%22%7D') > -1, 'has correct catalogSearch param'); 122 | assert.ok(src.indexOf('catalogSequence=%5B%7B%22q%22%3A%22cat-seq-1%22%7D%2C%7B%22q%22%3A%22cat-seq-2%22%7D%5D') > -1, 'has correct catalogSequence param'); 123 | }); 124 | 125 | QUnit.module('embed insertion', { 126 | beforeEach() { 127 | this.p = document.createElement('p'); 128 | 129 | // Add a paragraph element so we can verify that the various insertion 130 | // methods work properly. 131 | this.fixture.appendChild(this.p); 132 | }, 133 | afterEach() { 134 | this.p = null; 135 | } 136 | }); 137 | 138 | QUnit.test('"append" makes the embed the last child of the refNode', function(assert) { 139 | const embed = createEmbed({ 140 | refNode: this.fixture, 141 | refNodeInsert: 'append' 142 | }); 143 | 144 | assert.strictEqual(embed.parentNode, this.fixture, 'has the correct parentNode'); 145 | assert.strictEqual(embed, this.fixture.lastChild, 'was appended'); 146 | }); 147 | 148 | QUnit.test('"prepend" makes the embed the first child of the refNode', function(assert) { 149 | const embed = createEmbed({ 150 | refNode: this.fixture, 151 | refNodeInsert: 'prepend' 152 | }); 153 | 154 | assert.strictEqual(embed.parentNode, this.fixture, 'has the correct parentNode'); 155 | assert.strictEqual(embed, this.fixture.firstChild, 'was prepended'); 156 | }); 157 | 158 | QUnit.test('"before" makes the embed the previous sibling of the refNode', function(assert) { 159 | const embed = createEmbed({ 160 | refNode: this.p, 161 | refNodeInsert: 'before' 162 | }); 163 | 164 | assert.strictEqual(embed.parentNode, this.fixture, 'has the correct parentNode'); 165 | assert.strictEqual(embed.nextSibling, this.p, 'was added before p'); 166 | }); 167 | 168 | QUnit.test('"after" makes the embed the next sibling of the refNode', function(assert) { 169 | const embed = createEmbed({ 170 | refNode: this.p, 171 | refNodeInsert: 'after' 172 | }); 173 | 174 | assert.strictEqual(embed.parentNode, this.fixture, 'has the correct parentNode'); 175 | assert.strictEqual(embed.previousSibling, this.p, 'was added after p'); 176 | }); 177 | 178 | QUnit.test('"replace" makes the embed replace the refNode', function(assert) { 179 | const embed = createEmbed({ 180 | refNode: this.p, 181 | refNodeInsert: 'replace' 182 | }); 183 | 184 | assert.strictEqual(embed.parentNode, this.fixture, 'has the correct parentNode'); 185 | assert.strictEqual(this.p.parentNode, null, 'p was removed'); 186 | }); 187 | 188 | QUnit.module('embed options', { 189 | before() { 190 | 191 | this.isPipContainer = (assert, el) => { 192 | assert.ok(el.nodeName, 'DIV', 'is a div'); 193 | assert.ok(el.classList.contains('vjs-pip-container'), 'is a pip container'); 194 | }; 195 | 196 | this.nextSiblingIsPlaylistContainer = (assert, embed) => { 197 | assert.strictEqual(embed.nextSibling.nodeName, 'DIV', 'is a div'); 198 | assert.ok(embed.nextSibling.classList.contains('vjs-playlist'), 'is a playlist container'); 199 | }; 200 | 201 | this.hasResponsiveStyles = (assert, embed) => { 202 | assert.strictEqual(embed.style.position, 'absolute', 'embed has the expected style.position'); 203 | assert.strictEqual(embed.style.top, '0px', 'embed has the expected style.top'); 204 | assert.strictEqual(embed.style.right, '0px', 'embed has the expected style.right'); 205 | assert.strictEqual(embed.style.bottom, '0px', 'embed has the expected style.bottom'); 206 | assert.strictEqual(embed.style.left, '0px', 'embed has the expected style.left'); 207 | assert.strictEqual(embed.style.width, '100%', 'embed has the expected style.width'); 208 | assert.strictEqual(embed.style.height, '100%', 'embed has the expected style.height'); 209 | }; 210 | 211 | this.isResponsiveContainer = (assert, el, paddingTop = '56.25%', maxWidth = '100%') => { 212 | assert.strictEqual(el.nodeName, 'DIV', 'is a div'); 213 | assert.strictEqual(el.style.paddingTop, paddingTop, 'has the expected style.paddingTop'); 214 | assert.strictEqual(el.parentNode.nodeName, 'DIV', 'parent is a div'); 215 | assert.strictEqual(el.parentNode.style.position, 'relative', 'parent has the expected style.position'); 216 | assert.strictEqual(el.parentNode.style.display, 'block', 'parent has the expected style.display'); 217 | assert.strictEqual(el.parentNode.style.maxWidth, maxWidth, 'parent has the expected style.maxWidth'); 218 | }; 219 | } 220 | }); 221 | 222 | QUnit.test('pip', function(assert) { 223 | const embed = createEmbed({ 224 | refNode: this.fixture, 225 | refNodeInsert: 'append', 226 | embedOptions: { 227 | pip: true 228 | } 229 | }); 230 | 231 | assert.strictEqual(embed.parentNode.parentNode, this.fixture, 'has the expected relationship to the fixture'); 232 | this.isPipContainer(assert, embed.parentNode); 233 | }); 234 | 235 | QUnit.test('playlists', function(assert) { 236 | const embed = createEmbed({ 237 | refNode: this.fixture, 238 | refNodeInsert: 'append', 239 | embedOptions: { 240 | playlist: true 241 | } 242 | }); 243 | 244 | assert.strictEqual(embed.parentNode, this.fixture, 'has the expected relationship to the fixture'); 245 | this.nextSiblingIsPlaylistContainer(assert, embed); 246 | }); 247 | 248 | QUnit.test('responsive', function(assert) { 249 | const embed = createEmbed({ 250 | refNode: this.fixture, 251 | refNodeInsert: 'append', 252 | embedOptions: { 253 | responsive: true 254 | } 255 | }); 256 | 257 | assert.strictEqual(embed.parentNode.parentNode.parentNode, this.fixture, 'has the expected relationship to the fixture'); 258 | this.hasResponsiveStyles(assert, embed); 259 | this.isResponsiveContainer(assert, embed.parentNode); 260 | }); 261 | 262 | QUnit.test('responsive custom values', function(assert) { 263 | const embed = createEmbed({ 264 | refNode: this.fixture, 265 | refNodeInsert: 'append', 266 | embedOptions: { 267 | responsive: { 268 | aspectRatio: '4:3', 269 | maxWidth: '960px' 270 | } 271 | } 272 | }); 273 | 274 | assert.strictEqual(embed.parentNode.parentNode.parentNode, this.fixture, 'has the expected relationship to the fixture'); 275 | this.hasResponsiveStyles(assert, embed); 276 | this.isResponsiveContainer(assert, embed.parentNode, '75%', '960px'); 277 | }); 278 | 279 | QUnit.test('pip + playlists', function(assert) { 280 | const embed = createEmbed({ 281 | refNode: this.fixture, 282 | refNodeInsert: 'append', 283 | embedOptions: { 284 | pip: true, 285 | playlist: true 286 | } 287 | }); 288 | 289 | assert.strictEqual(embed.parentNode.parentNode, this.fixture, 'has the expected relationship to the fixture'); 290 | this.nextSiblingIsPlaylistContainer(assert, embed); 291 | this.isPipContainer(assert, embed.parentNode); 292 | }); 293 | 294 | QUnit.test('pip + responsive', function(assert) { 295 | const embed = createEmbed({ 296 | refNode: this.fixture, 297 | refNodeInsert: 'append', 298 | embedOptions: { 299 | pip: true, 300 | responsive: true 301 | } 302 | }); 303 | 304 | assert.strictEqual(embed.parentNode.parentNode.parentNode.parentNode, this.fixture, 'has the expected relationship to the fixture'); 305 | this.hasResponsiveStyles(assert, embed); 306 | this.isResponsiveContainer(assert, embed.parentNode); 307 | this.isPipContainer(assert, embed.parentNode.parentNode.parentNode); 308 | }); 309 | 310 | QUnit.test('playlists + responsive', function(assert) { 311 | const embed = createEmbed({ 312 | refNode: this.fixture, 313 | refNodeInsert: 'append', 314 | embedOptions: { 315 | playlist: true, 316 | responsive: true 317 | } 318 | }); 319 | 320 | assert.strictEqual(embed.parentNode.parentNode.parentNode, this.fixture, 'has the expected relationship to the fixture'); 321 | this.nextSiblingIsPlaylistContainer(assert, embed); 322 | this.hasResponsiveStyles(assert, embed); 323 | this.isResponsiveContainer(assert, embed.parentNode); 324 | }); 325 | 326 | QUnit.test('pip + playlists + responsive', function(assert) { 327 | const embed = createEmbed({ 328 | refNode: this.fixture, 329 | refNodeInsert: 'append', 330 | embedOptions: { 331 | pip: true, 332 | playlist: true, 333 | responsive: true 334 | } 335 | }); 336 | 337 | assert.strictEqual(embed.parentNode.parentNode.parentNode.parentNode, this.fixture, 'has the expected relationship to the fixture'); 338 | this.nextSiblingIsPlaylistContainer(assert, embed); 339 | this.hasResponsiveStyles(assert, embed); 340 | this.isResponsiveContainer(assert, embed.parentNode); 341 | this.isPipContainer(assert, embed.parentNode.parentNode.parentNode); 342 | }); 343 | 344 | QUnit.test('tagName', function(assert) { 345 | const embedOne = createEmbed({ 346 | refNode: this.fixture, 347 | refNodeInsert: 'append' 348 | }); 349 | 350 | assert.strictEqual(embedOne.tagName, 'VIDEO-JS', 'is a video-js element'); 351 | 352 | const embedTwo = createEmbed({ 353 | refNode: this.fixture, 354 | refNodeInsert: 'append', 355 | embedOptions: { 356 | tagName: 'video-js' 357 | } 358 | }); 359 | 360 | assert.strictEqual(embedTwo.tagName, 'VIDEO-JS', 'is a video-js element'); 361 | 362 | const embedThree = createEmbed({ 363 | refNode: this.fixture, 364 | refNodeInsert: 'append', 365 | embedOptions: { 366 | tagName: 'video' 367 | } 368 | }); 369 | 370 | assert.strictEqual(embedThree.tagName, 'VIDEO', 'is a video element'); 371 | 372 | const embedFour = createEmbed({ 373 | refNode: this.fixture, 374 | refNodeInsert: 'append', 375 | embedOptions: { 376 | tagName: 'div' 377 | } 378 | }); 379 | 380 | assert.strictEqual(embedFour.tagName, 'DIV', 'WILL create invalid embeds as it is not a public function'); 381 | }); 382 | 383 | QUnit.module('onEmbedCreated'); 384 | 385 | QUnit.test('a callback can be provided to customize the embed', function(assert) { 386 | const embed = createEmbed({ 387 | onEmbedCreated(el) { 388 | el.id = 'derp'; 389 | }, 390 | refNode: this.fixture, 391 | refNodeInsert: 'append' 392 | }); 393 | 394 | assert.strictEqual(embed.nodeName, 'VIDEO-JS', 'the embed is the correct element'); 395 | assert.strictEqual(embed.id, 'derp', 'the embed has the correct ID'); 396 | }); 397 | 398 | }); 399 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Brightcove Player Loader Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 23 |
24 |
25 |
26 |

Fill out the following form to run the Brightcove Player Loader with any Brightcove Player!

27 |

28 |
29 |
    30 |
  • Use the "Params" button above your player to see the parameters derived from the form inputs to construct that embed!
  • 31 |
  • In the browser console, in-page players are available on the global players object, keyed by their ID.
  • 32 |
  • If an embedded, in-page player does not look right, make sure it is a supported version!
  • 33 |
34 |
35 |
36 |
37 |
Basic Parameters
38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 |

52 | Using the following fields, all 53 | supported parameters 54 | are available via these form fields with the exception of 55 | onEmbedCreated, 56 | onFailure, 57 | onSuccess, 58 | Promise, 59 | refNode, 60 | and 61 | refNodeInsert. 62 |

63 |
64 |
65 |
66 |
67 | 70 |
71 |
72 |
73 |
74 |

Populate the player with media from the Playback API.

75 |
76 | 77 | 83 |
84 |
85 | 86 | 87 | 88 | Playlist ID and Video ID must be a number or 89 | reference id (including the ref:). 90 | Catalog Search and 91 | Sequence 92 | must be valid JSON (including a plain string). 93 | 94 |
95 |
96 | 97 | 98 | 99 | For more information, read Video Cloud SSAI Ad Config API. 100 | 101 |
102 |
103 | 104 | 105 | 106 | For more information, read TBD. 107 | 108 |
109 |
110 | 111 | 112 | 113 | A URL to a poster can be set for in-page embeds to override the poster returned from the Playback API. 114 | 115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | 125 |
126 |
127 |
128 |
129 |
130 | 131 | 135 | 136 | For more information, read Choosing the Correct Embed Code. 137 | 138 |
139 |
140 | 141 | 142 | 143 | For more information, read Guide: Embed APIs. Most users are not likely to change this parameter. 144 | 145 |
146 |
147 | 148 | 149 | 150 | Add an application ID to the generated embed code. 151 | 152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | 162 |
163 |
164 |
165 |
166 |

167 | The following fields can be used to configure Player Loader 168 | embed options. 169 |

170 |
171 | 172 | 176 |
177 |
178 |
179 |
180 | 181 | 182 |
183 |
184 |
185 |
186 | 187 | 192 |
193 |
194 | 195 | 196 |
197 |
198 | 199 | 200 |
201 |
202 |
203 |
204 | 205 | 206 |
207 |
208 | 209 | 210 |
211 |
212 | 213 | 218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 | 228 |
229 |
230 |
231 |
232 |
233 | 234 | 235 | 236 | Used to provide a custom URL for players that are not on the Brightcove production CDN, 237 | players.brightcove.net. Most users are not likely to change this parameter. 238 | 239 |
240 |
241 | 242 | 243 | 244 | Use valid JSON to specify 245 | Video.js options. 246 | Does not apply to iframe/Basic embeds. 247 | 248 |
249 |
250 |
251 |
252 |
253 |
254 | 255 |
256 |
257 |
258 |
259 |

Players will appear below...

260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |

268 | 269 | Copyright © Brightcove, Inc.
270 | Brightcove Player Loader is open source software released under the Apache-2.0 license.
271 | The Brightcove Player is not open-source or free-to-use; it is governed by the proprietary Brightcove Software License and is Copyright © Brightcove, Inc. 272 |
273 |

274 |
275 |
276 |
277 | 278 | 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brightcove Player Loader 2 | 3 | 4 | [![Build Status](https://travis-ci.org/brightcove/player-loader.svg?branch=master)](https://travis-ci.org/brightcove/player-loader) 5 | [![Greenkeeper badge](https://badges.greenkeeper.io/brightcove/player-loader.svg)](https://greenkeeper.io/) 6 | 7 | [![NPM](https://nodeico.herokuapp.com/@brightcove/player-loader.svg)](https://npmjs.com/package/@brightcove/player-loader) 8 | 9 | An asynchronous script loader for the Brightcove Player. 10 | 11 | 12 | 13 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 14 | 15 | - [License](#license) 16 | - [Why do I need this library?](#why-do-i-need-this-library) 17 | - [Brightcove Player Support](#brightcove-player-support) 18 | - [Browser Support](#browser-support) 19 | - [Installation](#installation) 20 | - [Inclusion](#inclusion) 21 | - [ES6 Modules (e.g. Rollup, Webpack)](#es6-modules-eg-rollup-webpack) 22 | - [CommonJS (e.g. Browserify)](#commonjs-eg-browserify) 23 | - [AMD (e.g. RequireJS)](#amd-eg-requirejs) 24 | - [` 133 | 136 | ``` 137 | 138 | ## Usage 139 | > Note: If you want to use this with React, we have an official react component called [@brightcove/react-player-loader](https://www.npmjs.com/package/@brightcove/react-player-loader) 140 | 141 | The Brightcove Player Loader exposes a single function. This function takes an parameters object which describes the player it should load and returns a `Promise` which resolves when the player is loaded and created and rejects if anything fails. 142 | 143 | ### Examples 144 | This is a minimal example, using only the required parameters, which will load a Video Cloud account's default player: 145 | 146 | ```js 147 | brightcovePlayerLoader({ 148 | refNode: someElement, 149 | accountId: '123456789' 150 | }); 151 | ``` 152 | 153 | This is a more complete example, using some optional parameters and handling the returned `Promise`: 154 | 155 | ```js 156 | brightcovePlayerLoader({ 157 | refNode: someElement, 158 | refNodeInsert: 'replace', 159 | accountId: '123456789', 160 | playerId: 'AbCDeFgHi', 161 | embedId: 'default', 162 | videoId: '987654321' 163 | }) 164 | .then(function(success) { 165 | // The player has been created! 166 | }) 167 | .catch(function(error) { 168 | // Player creation failed! 169 | }); 170 | ``` 171 | 172 | ### Pre-Existing Players 173 | This library will attempt to detect pre-existing players on the page. In other words, if this library runs after a Brightcove Player script was included earlier in the DOM, it will detect it and prevent additional downloads of the same player. For example: 174 | 175 | ``` 176 |
177 | 178 | 179 | 188 | ``` 189 | 190 | However, there is a limitation to this behavior. Players from separate accounts on the same page are not guaranteed to be properly detected. _This is considered an unsupported use-case._ 191 | 192 | ### Avoiding Downloads 193 | When used with [the related webpack plugin][webpack-plugin], you can take advantage of the Player Loader's embed creation capabilities while avoiding an additional, asynchronous request by bundling your Brightcove Player into your webpack bundle. 194 | 195 | Simply configure both libraries simultaneously and your player(s) should be bundled and no longer download asynchronously. 196 | 197 | ### Use of Promises or Callbacks 198 | By default, this library will look for a global `Promise`. However, you can explicitly provide a `Promise` implementation via the `Promise` parameter: 199 | 200 | ```js 201 | brightcovePlayerLoader({ 202 | refNode: someElement, 203 | accountId: '123456789', 204 | playerId: 'AbCDeFgHi', 205 | Promise: MyCustomPromise 206 | }) 207 | .then(function(success) { 208 | // The player has been created! 209 | }) 210 | .catch(function(error) { 211 | // Player creation failed! 212 | }); 213 | ``` 214 | 215 | Promises are not required for this library to work, but they are recommended. The only major browser that does not support `Promise` natively is IE11. We recommend polyfilling the `window.Promise` constructor if it does not exist. 216 | 217 | In cases where no `Promise` global exists and none is provided, the Brightcove Player Loader will return `undefined`. There are callbacks available, which can be used instead of `Promise` - passing them will prevent a `Promise` from being returned: 218 | 219 | ```js 220 | brightcovePlayerLoader({ 221 | refNode: someElement, 222 | accountId: '123456789', 223 | onSuccess: function(success) { 224 | // The player has been created! 225 | }, 226 | onFailure: function(error) { 227 | // Player creation failed! 228 | } 229 | }); 230 | ``` 231 | 232 | > **NOTE:** Providing these callbacks means opting-out of promises entirely. No `Promise` object will be returned! 233 | 234 | #### Success 235 | For resolved `Promise`s or `onSuccess` callbacks, a single argument will be passed. This "success object" can have the following properties: 236 | 237 | Property | Description 238 | ---------|------------ 239 | `type` | Either `'in-page'` or `'iframe'`, describing the type of embed that was created. 240 | `ref` | The type of value assigned to this property will vary by the `type`. For `'in-page'` embeds, it will be a [Video.js Player object][vjs-player] and for `'iframe'` embeds, it will be the `