├── .editorconfig ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── scripts ├── jsdoc.config.json ├── karma.conf.js └── rollup.config.js ├── src └── index.js ├── test └── index.test.js └── vendor └── 1 └── default_default ├── index.html ├── index.js └── index.min.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | .DS_Store 6 | ._* 7 | 8 | # Editors 9 | *~ 10 | *.swp 11 | *.tmproj 12 | *.tmproject 13 | *.sublime-* 14 | .idea/ 15 | .project/ 16 | .settings/ 17 | .vscode/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | 24 | # Dependency directories 25 | bower_components/ 26 | node_modules/ 27 | 28 | # Build-related directories 29 | dist/ 30 | docs/api/ 31 | test/dist/ 32 | .eslintcache 33 | .yo-rc.json 34 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [1.5.0](https://github.com/brightcove/react-player-loader/compare/v1.4.2...v1.5.0) (2024-02-05) 3 | 4 | ### Features 5 | 6 | * support strict mode ([#114](https://github.com/brightcove/react-player-loader/issues/114)) ([846d7a1](https://github.com/brightcove/react-player-loader/commit/846d7a1)) 7 | 8 | ### Bug Fixes 9 | 10 | * disable firefox from running in karma ([#121](https://github.com/brightcove/react-player-loader/issues/121)) ([ea097d0](https://github.com/brightcove/react-player-loader/commit/ea097d0)) 11 | 12 | 13 | ## [1.4.2](https://github.com/brightcove/react-player-loader/compare/v1.4.1...v1.4.2) (2022-07-19) 14 | 15 | ### Bug Fixes 16 | 17 | * Fix an issue where the updatePlayer method would throw when using iframe embeds ([#96](https://github.com/brightcove/react-player-loader/issues/96)) ([a184999](https://github.com/brightcove/react-player-loader/commit/a184999)) 18 | 19 | ### Chores 20 | 21 | * update jsdoc ([#95](https://github.com/brightcove/react-player-loader/issues/95)) ([2424025](https://github.com/brightcove/react-player-loader/commit/2424025)) 22 | 23 | 24 | ## [1.4.1](https://github.com/brightcove/react-player-loader/compare/v1.4.0...v1.4.1) (2020-10-09) 25 | 26 | ### Chores 27 | 28 | * **package:** Update [@brightcove](https://github.com/brightcove)/player-loader to 1.8.0 ([48c180b](https://github.com/brightcove/react-player-loader/commit/48c180b)) 29 | 30 | 31 | # [1.4.0](https://github.com/brightcove/react-player-loader/compare/v1.3.0...v1.4.0) (2020-01-31) 32 | 33 | ### Features 34 | 35 | * add support for manualReloadFromPropChanges prop ([#67](https://github.com/brightcove/react-player-loader/issues/67)) ([756af85](https://github.com/brightcove/react-player-loader/commit/756af85)) 36 | 37 | 38 | # [1.3.0](https://github.com/brightcove/react-player-loader/compare/v1.2.1...v1.3.0) (2019-07-22) 39 | 40 | ### Features 41 | 42 | * support changes to props ([#44](https://github.com/brightcove/react-player-loader/issues/44)) ([f742a2d](https://github.com/brightcove/react-player-loader/commit/f742a2d)) 43 | 44 | 45 | ## [1.2.1](https://github.com/brightcove/react-player-loader/compare/v1.2.0...v1.2.1) (2019-07-08) 46 | 47 | ### Bug Fixes 48 | 49 | * Fix missing dependency on [@brightcove](https://github.com/brightcove)/player-loader ([#42](https://github.com/brightcove/react-player-loader/issues/42)) ([af63590](https://github.com/brightcove/react-player-loader/commit/af63590)) 50 | 51 | 52 | # [1.2.0](https://github.com/brightcove/react-player-loader/compare/v1.1.2...v1.2.0) (2018-12-13) 53 | 54 | ### Features 55 | 56 | * Add support for attrs prop to customize component element ([#29](https://github.com/brightcove/react-player-loader/issues/29)) ([b1abf92](https://github.com/brightcove/react-player-loader/commit/b1abf92)) 57 | 58 | ### Chores 59 | 60 | * **package:** update dependencies ([#28](https://github.com/brightcove/react-player-loader/issues/28)) ([5250042](https://github.com/brightcove/react-player-loader/commit/5250042)) 61 | 62 | 63 | ## [1.1.2](https://github.com/brightcove/react-player-loader/compare/v1.1.1...v1.1.2) (2018-10-05) 64 | 65 | ### Chores 66 | 67 | * **package:** update all package versions ([ef58cde](https://github.com/brightcove/react-player-loader/commit/ef58cde)) 68 | * **package:** update videojs-standard to version 8.0.2 (#20) ([5d753b1](https://github.com/brightcove/react-player-loader/commit/5d753b1)), closes [#20](https://github.com/brightcove/react-player-loader/issues/20) 69 | 70 | 71 | ## [1.1.1](https://github.com/brightcove/react-player-loader/compare/v1.1.0...v1.1.1) (2018-09-17) 72 | 73 | ### Chores 74 | 75 | * Mark players as having been loaded with this library. (#18) ([8396842](https://github.com/brightcove/react-player-loader/commit/8396842)), closes [#18](https://github.com/brightcove/react-player-loader/issues/18) 76 | 77 | 78 | # [1.1.0](https://github.com/brightcove/react-player-loader/compare/v1.0.4...v1.1.0) (2018-09-12) 79 | 80 | ### Features 81 | 82 | * Use ref callback to add support for React 15 (#15) ([2c0161b](https://github.com/brightcove/react-player-loader/commit/2c0161b)), closes [#15](https://github.com/brightcove/react-player-loader/issues/15) 83 | 84 | ### Chores 85 | 86 | * **package:** Update [@brightcove](https://github.com/brightcove)/player-loader to 1.4.1 (#16) ([7fb309d](https://github.com/brightcove/react-player-loader/commit/7fb309d)), closes [#16](https://github.com/brightcove/react-player-loader/issues/16) 87 | * **package:** update videojs-generate-rollup-config to version 2.2.0 (#13) ([abcbe8a](https://github.com/brightcove/react-player-loader/commit/abcbe8a)), closes [#13](https://github.com/brightcove/react-player-loader/issues/13) 88 | 89 | 90 | ## [1.0.4](https://github.com/brightcove/react-player-loader/compare/v1.0.3...v1.0.4) (2018-09-05) 91 | 92 | ### Chores 93 | 94 | * **package:** Update [@brightcove](https://github.com/brightcove)/player-loader to fix install issues (#12) ([b19d5cf](https://github.com/brightcove/react-player-loader/commit/b19d5cf)), closes [#12](https://github.com/brightcove/react-player-loader/issues/12) 95 | 96 | 97 | ## [1.0.3](https://github.com/brightcove/react-player-loader/compare/v1.0.2...v1.0.3) (2018-09-05) 98 | 99 | ### Bug Fixes 100 | 101 | * Remove the postinstall script to prevent install issues (#11) ([c9f4b50](https://github.com/brightcove/react-player-loader/commit/c9f4b50)), closes [#11](https://github.com/brightcove/react-player-loader/issues/11) 102 | 103 | 104 | ## [1.0.2](https://github.com/brightcove/react-player-loader/compare/v1.0.1...v1.0.2) (2018-09-04) 105 | 106 | ### Bug Fixes 107 | 108 | * Fix an issue where the dist files for this package included some unexpected ES6 code. (#9) ([e6cb690](https://github.com/brightcove/react-player-loader/commit/e6cb690)), closes [#9](https://github.com/brightcove/react-player-loader/issues/9) 109 | 110 | ### Chores 111 | 112 | * **package:** Update dependencies to enable Greenkeeper 🌴 (#6) ([494e94e](https://github.com/brightcove/react-player-loader/commit/494e94e)), closes [#6](https://github.com/brightcove/react-player-loader/issues/6) 113 | 114 | 115 | ## [1.0.1](https://github.com/brightcove/react-player-loader/compare/v1.0.0...v1.0.1) (2018-08-30) 116 | 117 | ### Bug Fixes 118 | 119 | * Do not bundle React with the module builds and update tooling using the plugin generator v7.2.0 (#5) ([904885a](https://github.com/brightcove/react-player-loader/commit/904885a)), closes [#5](https://github.com/brightcove/react-player-loader/issues/5) 120 | 121 | 122 | # [1.0.0](https://github.com/brightcove/react-player-loader/compare/v0.2.0...v1.0.0) (2018-08-28) 123 | 124 | ### Documentation 125 | 126 | * Update README to clarify Brightcove Player version support. (#3) ([b98d120](https://github.com/brightcove/react-player-loader/commit/b98d120)), closes [#3](https://github.com/brightcove/react-player-loader/issues/3) 127 | 128 | 129 | # [0.2.0](https://github.com/brightcove/react-brightcove-player/compare/v0.1.0...v0.2.0) (2018-08-28) 130 | 131 | ### Features 132 | 133 | * Rename to [@brightcove](https://github.com/brightcove) scoped package. (#2) ([ded1597](https://github.com/brightcove/react-brightcove-player/commit/ded1597)), closes [#2](https://github.com/brightcove/react-brightcove-player/issues/2) 134 | 135 | 136 | # 0.1.0 (2018-08-20) 137 | 138 | ### Features 139 | 140 | * initial implementation (#1) ([ea8f47d](https://github.com/brightcove/react-brightcove-player/commit/ea8f47d)), closes [#1](https://github.com/brightcove/react-brightcove-player/issues/1) 141 | 142 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @brightcove/react-player-loader 2 | 3 | [![Build Status](https://travis-ci.org/brightcove/react-player-loader.svg?branch=master)](https://travis-ci.org/brightcove/react-player-loader) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/brightcove/react-player-loader.svg)](https://greenkeeper.io/) 5 | 6 | [![NPM](https://nodeico.herokuapp.com/@brightcove/react-player-loader.svg)](https://npmjs.com/package/@brightcove/react-player-loader) 7 | 8 | A React component to load a Brightcove Player in the browser. 9 | 10 | ## Brightcove Player Support 11 | 12 | This library has [the same support characteristics as the Brightcove Player Loader](https://github.com/brightcove/player-loader#brightcove-player-support). 13 | 14 | ## Table of Contents 15 | 16 | 17 | 18 | 19 | 20 | - [Installation](#installation) 21 | - [Standard Usage with JSX](#standard-usage-with-jsx) 22 | - [Props](#props) 23 | - [`attrs`](#attrs) 24 | - [`baseUrl`](#baseurl) 25 | - [`manualReloadFromPropChanges`](#manualreloadfrompropchanges) 26 | - [Other Props](#other-props) 27 | - [Effects of Prop Changes](#effects-of-prop-changes) 28 | - [View the Demo](#view-the-demo) 29 | - [Alternate Usage](#alternate-usage) 30 | - [ES Module (without JSX)](#es-module-without-jsx) 31 | - [CommonJS](#commonjs) 32 | - [` 204 | 205 | 206 | 222 | ``` 223 | 224 | [react]: https://www.npmjs.com/package/react 225 | [react-dom]: https://www.npmjs.com/package/react-dom 226 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Nothing will appear below because this page uses an invalid account ID by default.

8 |

Open the console and use window.customApp.setState({}) to try changing props.

9 |
10 | 11 | 12 | 13 | 14 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@brightcove/react-player-loader", 3 | "version": "1.5.0", 4 | "description": "The official react component for the Brightcove Player", 5 | "main": "dist/brightcove-react-player-loader.cjs.js", 6 | "module": "dist/brightcove-react-player-loader.es.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/brightcove/react-player-loader.git" 10 | }, 11 | "generator-videojs-plugin": { 12 | "version": "7.4.0" 13 | }, 14 | "browserslist": [ 15 | "defaults", 16 | "ie 11" 17 | ], 18 | "keywords": [ 19 | "audio", 20 | "brightcove", 21 | "media", 22 | "player", 23 | "react", 24 | "react-component", 25 | "video" 26 | ], 27 | "scripts": { 28 | "prebuild": "npm run clean", 29 | "build": "npm-run-all -p build:*", 30 | "build:js": "rollup -c scripts/rollup.config.js", 31 | "clean": "shx rm -rf ./dist ./test/dist", 32 | "postclean": "shx mkdir -p ./dist ./test/dist", 33 | "docs": "npm-run-all docs:*", 34 | "docs:api": "jsdoc src -c scripts/jsdoc.config.json -r -d docs/api", 35 | "docs:toc": "doctoc README.md", 36 | "lint": "vjsstandard", 37 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch", 38 | "start": "npm-run-all -p server watch", 39 | "pretest": "npm-run-all lint build", 40 | "test": "npm-run-all test:*", 41 | "posttest": "shx cat test/dist/coverage/text.txt", 42 | "test:unit": "karma start scripts/karma.conf.js", 43 | "test:verify": "vjsverify --verbose", 44 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s", 45 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md", 46 | "watch": "npm-run-all -p watch:*", 47 | "watch:js": "npm run build:js -- -w", 48 | "prepublishOnly": "npm-run-all build test:verify" 49 | }, 50 | "author": "Brightcove, Inc.", 51 | "license": "Apache-2.0", 52 | "vjsstandard": { 53 | "ignore": [ 54 | "dist", 55 | "docs", 56 | "test/dist", 57 | "vendor" 58 | ] 59 | }, 60 | "files": [ 61 | "CONTRIBUTING.md", 62 | "dist/", 63 | "docs/", 64 | "index.html", 65 | "scripts/", 66 | "src/", 67 | "test/" 68 | ], 69 | "dependencies": { 70 | "@brightcove/player-loader": "^1.8.0" 71 | }, 72 | "devDependencies": { 73 | "@testing-library/react": "^8.0.1", 74 | "conventional-changelog-cli": "^2.0.21", 75 | "conventional-changelog-videojs": "^3.0.0", 76 | "create-react-class": "^15.6.3", 77 | "doctoc": "^1.4.0", 78 | "husky": "^1.3.1", 79 | "in-publish": "^2.0.0", 80 | "jsdoc": "^3.6.10", 81 | "karma": "^4.1.0", 82 | "lint-staged": "^8.2.1", 83 | "not-prerelease": "^1.0.1", 84 | "npm-merge-driver-install": "^1.1.1", 85 | "npm-run-all": "^4.1.5", 86 | "pkg-ok": "^2.3.1", 87 | "react": "^18.2.0", 88 | "react-dom": "^18.2.0", 89 | "rollup": "^1.16.2", 90 | "shx": "^0.3.2", 91 | "sinon": "^7.3.2", 92 | "videojs-generate-karma-config": "^5.3.0", 93 | "videojs-generate-rollup-config": "^3.2.0", 94 | "videojs-generator-verify": "~1.2.0", 95 | "videojs-standard": "^8.0.3" 96 | }, 97 | "peerDependencies": { 98 | "react": ">=15.0.0" 99 | }, 100 | "husky": { 101 | "hooks": { 102 | "pre-commit": "lint-staged" 103 | } 104 | }, 105 | "lint-staged": { 106 | "*.js": [ 107 | "vjsstandard --fix", 108 | "git add" 109 | ], 110 | "README.md": [ 111 | "npm run docs:toc", 112 | "git add" 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /scripts/jsdoc.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | // TODO - currently firefox headless fails to run with karma, blocking the npm version script. 9 | // We should look into a better workaround that allows us to still run firefox through karma 10 | browsers(aboutToRun) { 11 | return aboutToRun.filter(launcherName => launcherName !== 'FirefoxHeadless'); 12 | }, 13 | files(defaults) { 14 | // defaults don't work for this project 15 | return [ 16 | 'node_modules/sinon/pkg/sinon.js', 17 | 'node_modules/react/umd/react.development.js', 18 | 'node_modules/react-dom/umd/react-dom.development.js', 19 | 'test/dist/bundle.js' 20 | ]; 21 | } 22 | }; 23 | 24 | config = generate(config, options); 25 | 26 | // any other custom stuff not supported by options here! 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /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-react-player-loader', 8 | exportName: 'BrightcoveReactPlayerLoader', 9 | externals(defaults) { 10 | return { 11 | browser: defaults.browser.concat([ 12 | 'react' 13 | ]), 14 | module: defaults.module.concat([ 15 | 'react' 16 | ]), 17 | test: defaults.test.concat([ 18 | 'react', 19 | 'react-dom' 20 | ]) 21 | }; 22 | }, 23 | globals(defaults) { 24 | return { 25 | browser: Object.assign(defaults.browser, { 26 | react: 'React' 27 | }), 28 | module: defaults.module, 29 | test: Object.assign(defaults.test, { 30 | 31 | // This is a deep dependency of @testing-library/react that doesn't 32 | // play nice with Rollup... 33 | '@sheerun/mutationobserver-shim': 'MutationObserver', 34 | 'react': 'React', 35 | 'react-dom': 'ReactDOM' 36 | }) 37 | }; 38 | } 39 | }; 40 | 41 | const config = generate(options); 42 | 43 | // Add additonal builds/customization here! 44 | 45 | // export the builds to rollup 46 | export default Object.values(config.builds); 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import playerLoader from '@brightcove/player-loader'; 3 | 4 | /** 5 | * These prop changes can be handled by an internal player state change rather 6 | * than a full dispose/recreate. 7 | * 8 | * @private 9 | * @type {Object} 10 | */ 11 | const UPDATEABLE_PROPS = [ 12 | 'catalogSearch', 13 | 'catalogSequence', 14 | 'playlistId', 15 | 'playlistVideoId', 16 | 'videoId' 17 | ]; 18 | 19 | const logError = (err) => { 20 | /* eslint-disable no-console */ 21 | if (err && console && console.error) { 22 | console.error(err); 23 | } 24 | /* eslint-enable no-console */ 25 | }; 26 | 27 | /** 28 | * The official React component for the Brightcove Player! 29 | * 30 | * This uses `@brightcove/player-loader` to load a player into a React 31 | * component based on the given props. 32 | */ 33 | class ReactPlayerLoader extends React.Component { 34 | 35 | /** 36 | * Create a new Brightcove player. 37 | * 38 | * @param {Object} props 39 | * Most options will be passed along to player-loader, except for 40 | * options that are listed. See README.md for more detail. 41 | * 42 | * @param {string} [props.baseUrl] 43 | * The base URL to use when requesting a player 44 | * 45 | * @param {Object} [props.attrs] 46 | * Used to set attributes on the component element that contains the 47 | * embedded Brightcove Player. 48 | * 49 | * @param {boolean} [props.manualReloadFromPropChanges] 50 | * Used to specify if reloading the player after prop changes will be handled manually. 51 | * 52 | */ 53 | constructor(props) { 54 | super(props); 55 | this.refNode = null; 56 | this.setRefNode = ref => { 57 | this.refNode = ref; 58 | }; 59 | this.loadPlayer = this.loadPlayer.bind(this); 60 | } 61 | 62 | /** 63 | * Loads a new player based on the current props. 64 | */ 65 | loadPlayer() { 66 | // Guard against loading the player twice, which would be caused by React's 67 | // strict mode unmounting and immediately re-mounting the component 68 | if (!this.loading_) { 69 | this.loading_ = true; 70 | } else { 71 | // eslint-disable-next-line no-console 72 | console.log('Brightcove React Player Loader aborted a subsequent player load while a load was pending'); 73 | return; 74 | } 75 | 76 | // If there is any player currently loaded, dispose it before fetching a 77 | // new one. 78 | this.disposePlayer(); 79 | 80 | // We need to provide our own callbacks below, so we cache these 81 | // user-provided callbacks for use later. 82 | const userSuccess = this.props.onSuccess; 83 | const userFailure = this.props.onFailure; 84 | 85 | const options = Object.assign({}, this.props, { 86 | refNode: this.refNode, 87 | refNodeInsert: 'append', 88 | onSuccess: ({ref, type}) => { 89 | // The player load process has completed. A subsequent load, 90 | // e.g. because of a props change, should be allowed 91 | this.loading_ = false; 92 | 93 | // If the component is not mounted when the callback fires, dispose 94 | // the player and bail out. 95 | if (!this.isMounted_) { 96 | this.disposePlayer(ref); 97 | return; 98 | } 99 | 100 | // Store a player reference on the component. 101 | this.player = ref; 102 | 103 | // Null out the player reference when the player is disposed from 104 | // outside the component. 105 | if (type === 'in-page') { 106 | ref.one('dispose', () => { 107 | this.player = null; 108 | }); 109 | } 110 | 111 | // Add a REACT_PLAYER_LOADER property to bcinfo to indicate this player 112 | // was loaded via that mechanism. 113 | if (ref.bcinfo) { 114 | ref.bcinfo.REACT_PLAYER_LOADER = true; 115 | } 116 | 117 | // Call a user-provided onSuccess callback. 118 | if (typeof userSuccess === 'function') { 119 | userSuccess({ref, type}); 120 | } 121 | }, 122 | onFailure: (error) => { 123 | // The player load process has completed. A subsequent load, 124 | // e.g. because of a props change, should be allowed 125 | this.loading_ = false; 126 | 127 | // Ignore errors when not mounted. 128 | if (!this.isMounted_) { 129 | return; 130 | } 131 | 132 | // Call a user-provided onFailure callback. 133 | if (typeof userFailure === 'function') { 134 | userFailure(error); 135 | return; 136 | } 137 | 138 | // Fall back to throwing an error; 139 | throw new Error(error); 140 | } 141 | }); 142 | 143 | // Delete props that are not meant to be passed to player-loader. 144 | delete options.attrs; 145 | delete options.baseUrl; 146 | delete options.manualReloadFromPropChanges; 147 | 148 | // If a base URL is provided, it should only apply to this player load. 149 | // This means we need to back up the original base URL and restore it 150 | // _after_ we call player loader. 151 | const originalBaseUrl = playerLoader.getBaseUrl(); 152 | 153 | if (this.props.baseUrl) { 154 | playerLoader.setBaseUrl(this.props.baseUrl); 155 | } 156 | 157 | playerLoader(options); 158 | playerLoader.setBaseUrl(originalBaseUrl); 159 | } 160 | 161 | /** 162 | * Disposes the current player, if there is one. 163 | */ 164 | disposePlayer() { 165 | 166 | // Nothing to dispose. 167 | if (!this.player) { 168 | return; 169 | } 170 | 171 | // Dispose an in-page player. 172 | if (this.player.dispose) { 173 | this.player.dispose(); 174 | 175 | // Dispose an iframe player. 176 | } else if (this.player.parentNode) { 177 | this.player.parentNode.removeChild(this.player); 178 | } 179 | 180 | // Null out the player reference. 181 | this.player = null; 182 | } 183 | 184 | /** 185 | * Find the index of the `playlistVideoId` prop within the player's playlist. 186 | * 187 | * @param {Object[]} playlist 188 | * An array of playlist item objects. 189 | * 190 | * @return {number} 191 | * The index of the `playlistVideoId` or `-1` if the player has been 192 | * disposed, is not using the playlist plugin, or if not found. 193 | */ 194 | findPlaylistVideoIdIndex_(playlist) { 195 | const {playlistVideoId} = this.props; 196 | 197 | if (Array.isArray(playlist) && playlistVideoId) { 198 | for (let i = 0; i < playlist.length; i++) { 199 | const {id, referenceId} = playlist[i]; 200 | 201 | if (id === playlistVideoId || `ref:${referenceId}` === playlistVideoId) { 202 | return i; 203 | } 204 | } 205 | } 206 | 207 | return -1; 208 | } 209 | 210 | /** 211 | * Create a Playback API callback function for the component's player. 212 | * 213 | * @private 214 | * @param {string} requestType 215 | * The Playback API request type (e.g. "video" or "playlist"). 216 | * 217 | * @param {Object} changes 218 | * An object. The keys of this object are the props that changed. 219 | * 220 | * @return {Function} 221 | * A callback for the Playback API request. 222 | */ 223 | createPlaybackAPICallback_(requestType, changes) { 224 | return (err, data) => { 225 | if (err) { 226 | logError(err); 227 | return; 228 | } 229 | 230 | // If the playlistVideoId changed and this is a playlist request, we 231 | // need to search through the playlist items to find the correct 232 | // starting index. 233 | if (requestType === 'playlist' && changes.playlistVideoId) { 234 | const i = this.findPlaylistVideoIdIndex_(data); 235 | 236 | if (i > -1) { 237 | data.startingIndex = i; 238 | } 239 | } 240 | 241 | this.player.catalog.load(data); 242 | }; 243 | } 244 | 245 | /** 246 | * Update the player based on changes to certain props that do not require 247 | * a full player dispose/recreate. 248 | * 249 | * @param {Object} changes 250 | * An object. The keys of this object are the props that changed. 251 | */ 252 | updatePlayer(changes) { 253 | 254 | // No player exists, player is disposed, or not using the catalog 255 | if (!this.player || !this.player.el || !this.player.el()) { 256 | return; 257 | } 258 | 259 | // If the player is using the catalog plugin, we _may_ populate this 260 | // variable with an object. 261 | let catalogParams; 262 | 263 | if (this.player.usingPlugin('catalog')) { 264 | 265 | // There is a new catalog sequence request. This takes precedence over 266 | // other catalog updates because it is a different call. 267 | if (changes.catalogSequence && this.props.catalogSequence) { 268 | const callback = this.createPlaybackAPICallback_('sequence', changes); 269 | 270 | this.player.catalog.getLazySequence(this.props.catalogSequence, callback, this.props.adConfigId); 271 | return; 272 | } 273 | 274 | if (changes.videoId && this.props.videoId) { 275 | catalogParams = { 276 | type: 'video', 277 | id: this.props.videoId 278 | }; 279 | } else if (changes.playlistId && this.props.playlistId) { 280 | catalogParams = { 281 | type: 'playlist', 282 | id: this.props.playlistId 283 | }; 284 | } else if (changes.catalogSearch && this.props.catalogSearch) { 285 | catalogParams = { 286 | type: 'search', 287 | q: this.props.catalogSearch 288 | }; 289 | } 290 | } 291 | 292 | // If `catalogParams` is `undefined` here, that means the player either 293 | // does not have the catalog plugin or no valid catalog request can be made. 294 | if (catalogParams) { 295 | if (this.props.adConfigId) { 296 | catalogParams.adConfigId = this.props.adConfigId; 297 | } 298 | 299 | if (this.props.deliveryConfigId) { 300 | catalogParams.deliveryConfigId = this.props.deliveryConfigId; 301 | } 302 | 303 | // We use the callback style here to make tests simpler in IE11 (no need 304 | // for a Promise polyfill). 305 | const callback = this.createPlaybackAPICallback_(catalogParams.type, changes); 306 | 307 | this.player.catalog.get(catalogParams, callback); 308 | 309 | // If no catalog request is being made, we may still need to update the 310 | // playlist selected video. 311 | } else if ( 312 | changes.playlistVideoId && 313 | this.props.playlistVideoId && 314 | this.player.usingPlugin('playlist') 315 | ) { 316 | const i = this.findPlaylistVideoIdIndex_(this.player.playlist()); 317 | 318 | if (i > -1) { 319 | this.player.playlist.currentItem(i); 320 | } 321 | } 322 | } 323 | 324 | /** 325 | * Called just after the component has mounted. 326 | */ 327 | componentDidMount() { 328 | this.isMounted_ = true; 329 | this.loadPlayer(); 330 | } 331 | 332 | /** 333 | * Called when the component props are updated. 334 | * 335 | * Some prop changes may trigger special behavior (see `propChangeHandlers`), 336 | * but if ANY prop is changed that is NOT handled, the player will be 337 | * disposed/recreated entirely. 338 | * 339 | * @param {Object} prevProps 340 | * The previous props state before change. 341 | */ 342 | componentDidUpdate(prevProps) { 343 | 344 | // Calculate the prop changes. 345 | const changes = Object.keys(prevProps).reduce((acc, key) => { 346 | const previous = prevProps[key]; 347 | const current = this.props[key]; 348 | 349 | // Do not compare functions 350 | if (typeof current === 'function') { 351 | return acc; 352 | } 353 | 354 | if (typeof current === 'object' && current !== null) { 355 | if (JSON.stringify(current) !== JSON.stringify(previous)) { 356 | acc[key] = true; 357 | } 358 | 359 | return acc; 360 | } 361 | 362 | if (current !== previous) { 363 | acc[key] = true; 364 | } 365 | 366 | return acc; 367 | }, {}); 368 | 369 | if (!this.props.manualReloadFromPropChanges) { 370 | // Dispose and recreate the player if any changed keys cannot be handled. 371 | if (Object.keys(changes).some(k => UPDATEABLE_PROPS.indexOf(k) === -1)) { 372 | this.loadPlayer(); 373 | return; 374 | } 375 | } 376 | 377 | this.updatePlayer(changes); 378 | } 379 | 380 | /** 381 | * Called just before a component unmounts. Disposes the player. 382 | */ 383 | componentWillUnmount() { 384 | this.isMounted_ = false; 385 | this.disposePlayer(); 386 | } 387 | 388 | /** 389 | * Renders the component. 390 | * 391 | * @return {ReactElement} 392 | * The react element to render. 393 | */ 394 | render() { 395 | const props = Object.assign( 396 | {className: 'brightcove-react-player-loader'}, 397 | this.props.attrs, 398 | {ref: this.setRefNode} 399 | ); 400 | 401 | return React.createElement('div', props); 402 | } 403 | } 404 | 405 | export default ReactPlayerLoader; 406 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | import window from 'global/window'; 3 | import QUnit from 'qunit'; 4 | import sinon from 'sinon'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import {render, cleanup} from '@testing-library/react'; 8 | import ReactPlayerLoader from '../src/index.js'; 9 | import BrightcovePlayerLoader from '@brightcove/player-loader'; 10 | 11 | QUnit.module('ReactPlayerLoader', { 12 | beforeEach() { 13 | this.fixture = document.getElementById('qunit-fixture'); 14 | this.originalBaseUrl = BrightcovePlayerLoader.getBaseUrl(); 15 | BrightcovePlayerLoader.setBaseUrl(`${window.location.origin}/vendor/`); 16 | }, 17 | afterEach() { 18 | cleanup(); 19 | 20 | // reset the base url and global state 21 | BrightcovePlayerLoader.setBaseUrl(this.originalBaseUrl); 22 | BrightcovePlayerLoader.reset(); 23 | 24 | // unmount all components on the fixture 25 | ReactDOM.unmountComponentAtNode(this.fixture); 26 | } 27 | }, function() { 28 | 29 | QUnit.module('baseline usage'); 30 | 31 | QUnit.test('failure', function(assert) { 32 | const done = assert.async(); 33 | 34 | assert.expect(2); 35 | 36 | const reactPlayerLoader = ReactDOM.render( 37 | React.createElement(ReactPlayerLoader, { 38 | accountId: '2', 39 | onFailure: (failure) => { 40 | assert.ok(failure, 'failed to download non-existent player'); 41 | done(); 42 | } 43 | }), 44 | this.fixture 45 | ); 46 | 47 | assert.ok(reactPlayerLoader, 'player loader react component created'); 48 | }); 49 | 50 | QUnit.test('success', function(assert) { 51 | const done = assert.async(); 52 | 53 | assert.expect(2); 54 | 55 | const reactPlayerLoader = ReactDOM.render( 56 | React.createElement(ReactPlayerLoader, { 57 | accountId: '1', 58 | onSuccess: ({ref, type}) => { 59 | assert.ok(ref, 'downloaded and created a player'); 60 | done(); 61 | } 62 | }), 63 | this.fixture 64 | ); 65 | 66 | assert.ok(reactPlayerLoader, 'player loader react component created'); 67 | }); 68 | 69 | QUnit.test('unmount after success', function(assert) { 70 | const done = assert.async(); 71 | 72 | assert.expect(2); 73 | 74 | const reactPlayerLoader = ReactDOM.render( 75 | React.createElement(ReactPlayerLoader, { 76 | accountId: '1', 77 | onSuccess: ({ref, type}) => { 78 | assert.ok(ref, 'downloaded and created a player'); 79 | window.setTimeout(() => { 80 | ReactDOM.unmountComponentAtNode(this.fixture); 81 | done(); 82 | }, 1); 83 | } 84 | }), 85 | this.fixture 86 | ); 87 | 88 | assert.ok(reactPlayerLoader, 'player loader react component created'); 89 | }); 90 | 91 | QUnit.module('attrs'); 92 | 93 | QUnit.test('can set attributes on the component element', function(assert) { 94 | const done = assert.async(); 95 | 96 | assert.expect(1); 97 | 98 | ReactDOM.render( 99 | React.createElement(ReactPlayerLoader, { 100 | accountId: '1', 101 | attrs: {foo: 'bar'}, 102 | onSuccess: ({ref, type}) => { 103 | window.setTimeout(done, 1); 104 | } 105 | }), 106 | this.fixture 107 | ); 108 | 109 | assert.ok(this.fixture.querySelector('div[foo="bar"]'), 'foo="bar" div exists'); 110 | }); 111 | 112 | QUnit.test('className defaults to "brightcove-react-player-loader"', function(assert) { 113 | const done = assert.async(); 114 | 115 | assert.expect(1); 116 | 117 | ReactDOM.render( 118 | React.createElement(ReactPlayerLoader, { 119 | accountId: '1', 120 | attrs: {id: 'test'}, 121 | onSuccess: ({ref, type}) => { 122 | window.setTimeout(done, 1); 123 | } 124 | }), 125 | this.fixture 126 | ); 127 | 128 | const el = this.fixture.querySelector('#test'); 129 | 130 | assert.strictEqual(el.className, 'brightcove-react-player-loader', 'component element has correct className'); 131 | }); 132 | 133 | QUnit.test('className can be set to override default', function(assert) { 134 | const done = assert.async(); 135 | 136 | assert.expect(1); 137 | 138 | ReactDOM.render( 139 | React.createElement(ReactPlayerLoader, { 140 | accountId: '1', 141 | attrs: { 142 | className: 'foo bar', 143 | id: 'test' 144 | }, 145 | onSuccess: ({ref, type}) => { 146 | window.setTimeout(done, 1); 147 | } 148 | }), 149 | this.fixture 150 | ); 151 | 152 | const el = this.fixture.querySelector('#test'); 153 | 154 | assert.strictEqual(el.className, 'foo bar', 'component element has correct className'); 155 | }); 156 | 157 | QUnit.module('props', { 158 | beforeEach() { 159 | 160 | // This mock catalog plugin takes custom options for testing purposes. 161 | // The ReactPlayerLoader expects that the player will have initialized 162 | // the Video Cloud Catalog plugin itself. 163 | // 164 | // The custom options for the mock plugin instruct it whether or not to 165 | // produce "error" cases. 166 | this.mockCatalogPlugin = function(options = {}) { 167 | const err = new Error('mock catalog request failure'); 168 | 169 | this.catalog = { 170 | getLazySequence(seq, cb, adConfigId) { 171 | if (options.error) { 172 | cb(err, null); 173 | } else { 174 | cb(null, options.data || [{foo: 1}]); 175 | } 176 | }, 177 | get(params, cb) { 178 | let result = {foo: 1}; 179 | 180 | if (params.type === 'playlist' || params.type === 'search') { 181 | result = [result, {foo: 2}]; 182 | } 183 | 184 | if (options.error) { 185 | cb(err, null); 186 | } else { 187 | cb(null, options.data || result); 188 | } 189 | }, 190 | load(data) { 191 | return data; 192 | } 193 | }; 194 | 195 | Object.keys(this.catalog).forEach(key => sinon.spy(this.catalog, key)); 196 | }; 197 | 198 | this.mockPlaylistPlugin = function(list = [], currentItem = 0) { 199 | this.playlist = (l, i) => { 200 | if (l !== undefined) { 201 | list = l; 202 | if (i > -1) { 203 | currentItem = i; 204 | } 205 | } 206 | return list; 207 | }; 208 | 209 | this.playlist.currentItem = (i) => { 210 | if (i !== undefined) { 211 | currentItem = i; 212 | } 213 | return currentItem; 214 | }; 215 | 216 | sinon.spy(this.playlist, 'currentItem'); 217 | }; 218 | }, 219 | afterEach() { 220 | this.mockCatalogPlugin = null; 221 | this.mockPlaylistPlugin = null; 222 | } 223 | }); 224 | 225 | QUnit.test('change catalogSearch, success response', function(assert) { 226 | const done = assert.async(); 227 | 228 | let rerender; 229 | 230 | const props = { 231 | accountId: '1', 232 | adConfigId: 'abc-123', 233 | deliveryConfigId: 'def-456', 234 | catalogSearch: '2', 235 | onSuccess: ({ref, type}) => { 236 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 237 | ref.catalog(); 238 | 239 | props.catalogSearch = '3'; 240 | 241 | rerender(React.createElement(ReactPlayerLoader, props)); 242 | 243 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 244 | assert.ok(ref.catalog.load.calledOnce, 'player.catalog.load was called'); 245 | 246 | const params = ref.catalog.get.getCall(0).args[0]; 247 | 248 | assert.deepEqual(params, { 249 | adConfigId: 'abc-123', 250 | deliveryConfigId: 'def-456', 251 | q: '3', 252 | type: 'search' 253 | }, 'catalog params were correct'); 254 | 255 | done(); 256 | } 257 | }; 258 | 259 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 260 | }); 261 | 262 | QUnit.test('change catalogSearch, failure response', function(assert) { 263 | const done = assert.async(); 264 | 265 | let rerender; 266 | 267 | const props = { 268 | accountId: '1', 269 | catalogSearch: '2', 270 | onSuccess: ({ref, type}) => { 271 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 272 | ref.catalog({error: true}); 273 | 274 | props.catalogSearch = '3'; 275 | 276 | rerender(React.createElement(ReactPlayerLoader, props)); 277 | 278 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 279 | assert.ok(ref.catalog.load.notCalled, 'player.catalog.load was not called'); 280 | 281 | done(); 282 | } 283 | }; 284 | 285 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 286 | }); 287 | 288 | QUnit.test('change catalogSequence, success response', function(assert) { 289 | const done = assert.async(); 290 | 291 | let rerender; 292 | 293 | const props = { 294 | accountId: '1', 295 | adConfigId: 'abc-123', 296 | deliveryConfigId: 'def-456', 297 | catalogSequence: [{}], 298 | onSuccess: ({ref, type}) => { 299 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 300 | ref.catalog(); 301 | 302 | props.catalogSequence = [{}, {}]; 303 | 304 | rerender(React.createElement(ReactPlayerLoader, props)); 305 | 306 | assert.ok(ref.catalog.get.notCalled, 'player.catalog.get was not called'); 307 | assert.ok(ref.catalog.getLazySequence.calledOnce, 'player.catalog.getLazySequence was called'); 308 | assert.ok(ref.catalog.load.calledOnce, 'player.catalog.load was called'); 309 | 310 | const seq = ref.catalog.getLazySequence.getCall(0).args[0]; 311 | 312 | assert.deepEqual(seq, props.catalogSequence, 'catalog sequence was correct'); 313 | 314 | done(); 315 | } 316 | }; 317 | 318 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 319 | }); 320 | 321 | QUnit.test('change catalogSequence, failure response', function(assert) { 322 | const done = assert.async(); 323 | 324 | let rerender; 325 | 326 | const props = { 327 | accountId: '1', 328 | catalogSequence: [{}], 329 | onSuccess: ({ref, type}) => { 330 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 331 | ref.catalog({error: true}); 332 | 333 | props.catalogSequence = [{}, {}]; 334 | 335 | rerender(React.createElement(ReactPlayerLoader, props)); 336 | 337 | assert.ok(ref.catalog.get.notCalled, 'player.catalog.get was not called'); 338 | assert.ok(ref.catalog.getLazySequence.calledOnce, 'player.catalog.getLazySequence was called'); 339 | assert.ok(ref.catalog.load.notCalled, 'player.catalog.load was not called'); 340 | 341 | done(); 342 | } 343 | }; 344 | 345 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 346 | }); 347 | 348 | QUnit.test('change playlistId, success response', function(assert) { 349 | const done = assert.async(); 350 | 351 | let rerender; 352 | 353 | const props = { 354 | accountId: '1', 355 | adConfigId: 'abc-123', 356 | deliveryConfigId: 'def-456', 357 | playlistId: '2', 358 | onSuccess: ({ref, type}) => { 359 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 360 | ref.catalog(); 361 | 362 | props.playlistId = '3'; 363 | 364 | rerender(React.createElement(ReactPlayerLoader, props)); 365 | 366 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 367 | assert.ok(ref.catalog.load.calledOnce, 'player.catalog.load was called'); 368 | 369 | const params = ref.catalog.get.getCall(0).args[0]; 370 | 371 | assert.deepEqual(params, { 372 | adConfigId: 'abc-123', 373 | deliveryConfigId: 'def-456', 374 | id: '3', 375 | type: 'playlist' 376 | }, 'catalog params were correct'); 377 | 378 | done(); 379 | } 380 | }; 381 | 382 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 383 | }); 384 | 385 | QUnit.test('change playlistId, failure response', function(assert) { 386 | const done = assert.async(); 387 | 388 | let rerender; 389 | 390 | const props = { 391 | accountId: '1', 392 | playlistId: '2', 393 | onSuccess: ({ref, type}) => { 394 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 395 | ref.catalog({error: true}); 396 | 397 | props.playlistId = '3'; 398 | 399 | rerender(React.createElement(ReactPlayerLoader, props)); 400 | 401 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 402 | assert.ok(ref.catalog.load.notCalled, 'player.catalog.load was not called'); 403 | 404 | done(); 405 | } 406 | }; 407 | 408 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 409 | }); 410 | 411 | QUnit.test('change videoId, success response', function(assert) { 412 | const done = assert.async(); 413 | 414 | let rerender; 415 | 416 | const props = { 417 | accountId: '1', 418 | adConfigId: 'abc-123', 419 | deliveryConfigId: 'def-456', 420 | videoId: '2', 421 | onSuccess: ({ref, type}) => { 422 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 423 | ref.catalog(); 424 | 425 | props.videoId = '3'; 426 | 427 | rerender(React.createElement(ReactPlayerLoader, props)); 428 | 429 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 430 | assert.ok(ref.catalog.load.calledOnce, 'player.catalog.load was called'); 431 | 432 | const params = ref.catalog.get.getCall(0).args[0]; 433 | 434 | assert.deepEqual(params, { 435 | adConfigId: 'abc-123', 436 | deliveryConfigId: 'def-456', 437 | id: '3', 438 | type: 'video' 439 | }, 'catalog params were correct'); 440 | 441 | done(); 442 | } 443 | }; 444 | 445 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 446 | }); 447 | 448 | QUnit.test('change videoId, failure response', function(assert) { 449 | const done = assert.async(); 450 | 451 | let rerender; 452 | 453 | const props = { 454 | accountId: '1', 455 | videoId: '2', 456 | onSuccess: ({ref, type}) => { 457 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 458 | ref.catalog({error: true}); 459 | 460 | props.videoId = '3'; 461 | 462 | rerender(React.createElement(ReactPlayerLoader, props)); 463 | 464 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 465 | assert.ok(ref.catalog.load.notCalled, 'player.catalog.load was not called'); 466 | 467 | done(); 468 | } 469 | }; 470 | 471 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 472 | }); 473 | 474 | QUnit.test('change from one catalog request type to another ignores previous props', function(assert) { 475 | const done = assert.async(); 476 | 477 | let rerender; 478 | 479 | const props = { 480 | accountId: '1', 481 | adConfigId: 'abc-123', 482 | deliveryConfigId: 'def-456', 483 | catalogSearch: '0', 484 | catalogSequence: '1', 485 | playlistId: '2', 486 | videoId: '3', 487 | onSuccess: ({ref, type}) => { 488 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 489 | ref.catalog(); 490 | 491 | props.playlistId = '4'; 492 | 493 | rerender(React.createElement(ReactPlayerLoader, props)); 494 | 495 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 496 | assert.ok(ref.catalog.load.calledOnce, 'player.catalog.load was called'); 497 | 498 | const params = ref.catalog.get.getCall(0).args[0]; 499 | 500 | assert.deepEqual(params, { 501 | adConfigId: 'abc-123', 502 | deliveryConfigId: 'def-456', 503 | id: '4', 504 | type: 'playlist' 505 | }, 'props that trigger a type of catalog request and were not changed were not included'); 506 | 507 | done(); 508 | } 509 | }; 510 | 511 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 512 | }); 513 | 514 | QUnit.test('changing playlistId + playlistVideoId, success response', function(assert) { 515 | const done = assert.async(); 516 | 517 | let rerender; 518 | 519 | const props = { 520 | accountId: '1', 521 | embedOptions: {unminified: true}, 522 | playlistId: '0', 523 | playlistVideoId: '0', 524 | onSuccess: ({ref, type}) => { 525 | const playlist = [{ 526 | id: 1 527 | }, { 528 | id: 2 529 | }]; 530 | 531 | window.videojs.registerPlugin('catalog', this.mockCatalogPlugin); 532 | ref.catalog({data: playlist}); 533 | 534 | props.playlistId = '1'; 535 | props.playlistVideoId = 2; 536 | 537 | rerender(React.createElement(ReactPlayerLoader, props)); 538 | 539 | assert.ok(ref.catalog.get.calledOnce, 'player.catalog.get was called'); 540 | assert.ok(ref.catalog.load.calledOnce, 'player.catalog.load was called'); 541 | 542 | const params = ref.catalog.get.getCall(0).args[0]; 543 | 544 | assert.deepEqual(params, { 545 | id: '1', 546 | type: 'playlist' 547 | }, 'catalog params were correct'); 548 | 549 | // The test players have the playlist plugin, but we want a minimal 550 | // test case for playlists; so, we remove it and mock a new plugin. 551 | window.videojs.getPlugin('plugin').deregisterPlugin('playlist'); 552 | delete ref.playlist; 553 | 554 | window.videojs.registerPlugin('playlist', this.mockPlaylistPlugin); 555 | ref.playlist(playlist, playlist.startingIndex); 556 | 557 | assert.strictEqual(ref.playlist.currentItem(), 1, 'the second playlist item is selected'); 558 | 559 | done(); 560 | } 561 | }; 562 | 563 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 564 | }); 565 | 566 | QUnit.test('changing playlistVideoId on an already loaded playlist, using referenceId instead of id', function(assert) { 567 | const done = assert.async(); 568 | 569 | let rerender; 570 | 571 | const props = { 572 | accountId: '1', 573 | playlistId: '0', 574 | playlistVideoId: '0', 575 | onSuccess: ({ref, type}) => { 576 | 577 | // The test players have the playlist plugin, but we want a minimal 578 | // test case for playlists; so, we remove it and mock a new plugin. 579 | window.videojs.getPlugin('plugin').deregisterPlugin('playlist'); 580 | delete ref.playlist; 581 | 582 | window.videojs.registerPlugin('playlist', this.mockPlaylistPlugin); 583 | ref.playlist([{ 584 | id: 1 585 | }, { 586 | id: 2 587 | }, { 588 | id: 3, 589 | referenceId: 'foo' 590 | }]); 591 | 592 | props.playlistVideoId = 'ref:foo'; 593 | 594 | rerender(React.createElement(ReactPlayerLoader, props)); 595 | 596 | assert.strictEqual(ref.playlist.currentItem(), 2, 'the third playlist item is selected'); 597 | 598 | done(); 599 | } 600 | }; 601 | 602 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 603 | }); 604 | 605 | QUnit.test('other prop changes reload the player', function(assert) { 606 | const done = assert.async(); 607 | 608 | let rerender; 609 | 610 | const props = { 611 | accountId: '1', 612 | applicationId: 'foo', 613 | onSuccess: ({ref, type}) => { 614 | if (props.applicationId === 'bar') { 615 | assert.ok(true, 'the success callback was called a second time because the player was re-loaded'); 616 | done(); 617 | } 618 | 619 | props.applicationId = 'bar'; 620 | rerender(React.createElement(ReactPlayerLoader, props)); 621 | } 622 | }; 623 | 624 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 625 | }); 626 | 627 | QUnit.test('loadPlayer() method reloads player', function(assert) { 628 | const done = assert.async(2); 629 | 630 | const props = { 631 | accountId: '1', 632 | applicationId: 'foo', 633 | onSuccess: ({ref, type}) => { 634 | assert.ok(true, 'the success callback was called'); 635 | done(); 636 | } 637 | }; 638 | 639 | const reactPlayerLoader = ReactDOM.render( 640 | React.createElement(ReactPlayerLoader, props), 641 | this.fixture 642 | ); 643 | 644 | // Add a delay, otherwise this is emulating the strict mode problem 645 | window.setTimeout(function() { 646 | reactPlayerLoader.loadPlayer(); 647 | }, 1000); 648 | }); 649 | 650 | QUnit.test('set manualReloadFromPropChanges to true', function(assert) { 651 | const done = assert.async(2); 652 | let rerender; 653 | const props = { 654 | accountId: '1', 655 | applicationId: 'foo', 656 | manualReloadFromPropChanges: true, 657 | onSuccess: ({ref, type}) => { 658 | if (props.applicationId !== 'bar') { 659 | props.applicationId = 'bar'; 660 | done(); 661 | } 662 | rerender(React.createElement(ReactPlayerLoader, props)); 663 | assert.ok(true, 'the success callback was called'); 664 | done(); 665 | } 666 | }; 667 | 668 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 669 | }); 670 | 671 | QUnit.test('set manualReloadFromPropChanges to false', function(assert) { 672 | const done = assert.async(3); 673 | let rerender; 674 | const props = { 675 | accountId: '1', 676 | applicationId: 'foo', 677 | manualReloadFromPropChanges: false, 678 | onSuccess: ({ref, type}) => { 679 | if (props.applicationId !== 'bar') { 680 | props.applicationId = 'bar'; 681 | done(); 682 | } 683 | rerender(React.createElement(ReactPlayerLoader, props)); 684 | assert.ok(true, 'the success callback was called'); 685 | done(); 686 | } 687 | }; 688 | 689 | rerender = render(React.createElement(ReactPlayerLoader, props)).rerender; 690 | }); 691 | 692 | QUnit.module('strict mode'); 693 | 694 | QUnit.test('player not created twice in strict mode', function(assert) { 695 | const done = assert.async(); 696 | 697 | assert.expect(1); 698 | 699 | // createRoot needed for React 18 700 | const root = ReactDOM.createRoot(this.fixture); 701 | 702 | root.render(React.createElement( 703 | React.StrictMode, 704 | {}, 705 | React.createElement(ReactPlayerLoader, { 706 | accountId: '1', 707 | onSuccess: ({ref, type}) => { 708 | assert.ok(ref, 'downloaded and created a player'); 709 | } 710 | }) 711 | )); 712 | 713 | window.setTimeout(function() { 714 | root.unmount(); 715 | done(); 716 | }, 1000); 717 | 718 | }); 719 | 720 | QUnit.test('aborted player load logs message', function(assert) { 721 | const done = assert.async(); 722 | const spy = sinon.spy(window.console, 'log'); 723 | 724 | assert.expect(1); 725 | 726 | // createRoot needed for React 18 727 | const root = ReactDOM.createRoot(this.fixture); 728 | 729 | root.render(React.createElement( 730 | React.StrictMode, 731 | {}, 732 | React.createElement(ReactPlayerLoader, { 733 | accountId: '1' 734 | }) 735 | )); 736 | 737 | window.setTimeout(function() { 738 | assert.ok( 739 | spy.calledWith('Brightcove React Player Loader aborted a subsequent player load while a load was pending'), 740 | 'warning logged' 741 | ); 742 | root.unmount(); 743 | spy.restore(); 744 | done(); 745 | }, 1000); 746 | }); 747 | }); 748 | --------------------------------------------------------------------------------