├── .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 | 'Params ',
197 | ' ',
198 | 'Share ',
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('This player appears to be an unsupported version! Brightcove Player Loader supports v6.0.0 and higher for in-page embeds.
');
251 | }
252 | }, function(err) {
253 | refNode.append('' + err + '
');
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 |
21 | Brightcove Player Loader Demo
22 |
23 |
24 |
25 |
26 |
Fill out the following form to run the Brightcove Player Loader with any Brightcove Player!
27 |
Show some extra usage tips…
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 |
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 | [](https://travis-ci.org/brightcove/player-loader)
5 | [](https://greenkeeper.io/)
6 |
7 | [](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 `