├── .nvmrc ├── .jsdoc ├── .editorconfig ├── .eslintrc.json ├── src ├── scss │ ├── videojs-chromecast.scss │ ├── _chromecastButton.scss │ └── _tech.scss ├── images │ ├── ic_cast_black_24dp.png │ ├── ic_cast_blue_24dp.png │ ├── ic_cast_grey_24dp.png │ ├── ic_cast_white_24dp.png │ ├── ic_cast_connected_black_24dp.png │ ├── ic_cast_connected_blue_24dp.png │ ├── ic_cast_connected_grey_24dp.png │ └── ic_cast_connected_white_24dp.png ├── .eslintrc.json └── js │ ├── standalone.js │ ├── index.js │ ├── preloadWebComponents.js │ ├── tech │ ├── ChromecastTechUI.js │ └── ChromecastTech.js │ ├── components │ └── ChromecastButton.js │ ├── enableChromecast.js │ └── chromecast │ └── ChromecastSessionManager.js ├── .markdownlint-cli2.cjs ├── .stylelintrc.yml ├── .browserslistrc ├── tests ├── .eslintrc.json ├── Placeholder.test.js ├── ChromcastButton.test.js └── ChromcastTech.test.js ├── .npmignore ├── commitlint.config.js ├── .nycrc.json ├── .gitignore ├── LICENSE ├── docs └── demo │ └── index.html ├── CHANGELOG.md ├── .github └── workflows │ └── ci.yml ├── package.json ├── Gruntfile.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.12.2 2 | -------------------------------------------------------------------------------- /.jsdoc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "plugins/markdown" ] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ./node_modules/@silvermine/standardization/.editorconfig -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/node" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/scss/videojs-chromecast.scss: -------------------------------------------------------------------------------- 1 | @import 'chromecastButton'; 2 | @import 'tech'; 3 | -------------------------------------------------------------------------------- /.markdownlint-cli2.cjs: -------------------------------------------------------------------------------- 1 | node_modules/@silvermine/standardization/.markdownlint-cli2.shared.cjs -------------------------------------------------------------------------------- /.stylelintrc.yml: -------------------------------------------------------------------------------- 1 | extends: ./node_modules/@silvermine/standardization/.stylelintrc.yml 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | ./node_modules/@silvermine/standardization/browserslist/.browserslistrc-broad-support -------------------------------------------------------------------------------- /tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/node-tests" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.json 2 | .travis.yml 3 | .npmignore 4 | Gruntfile.js 5 | tests/** 6 | .nyc_output 7 | coverage 8 | -------------------------------------------------------------------------------- /src/images/ic_cast_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_black_24dp.png -------------------------------------------------------------------------------- /src/images/ic_cast_blue_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_blue_24dp.png -------------------------------------------------------------------------------- /src/images/ic_cast_grey_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_grey_24dp.png -------------------------------------------------------------------------------- /src/images/ic_cast_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_white_24dp.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: [ '@silvermine/standardization/commitlint.js' ], 5 | }; 6 | -------------------------------------------------------------------------------- /src/images/ic_cast_connected_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_connected_black_24dp.png -------------------------------------------------------------------------------- /src/images/ic_cast_connected_blue_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_connected_blue_24dp.png -------------------------------------------------------------------------------- /src/images/ic_cast_connected_grey_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_connected_grey_24dp.png -------------------------------------------------------------------------------- /src/images/ic_cast_connected_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvermine/videojs-chromecast/HEAD/src/images/ic_cast_connected_white_24dp.png -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/browser", 4 | "parser": "babel-eslint", 5 | "globals": { 6 | "chrome": true, 7 | "cast": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/Placeholder.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('expect.js'); 4 | 5 | describe('Everything', function() { 6 | 7 | it('needs to be tested', function() { 8 | expect(true).to.be(true); 9 | }); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /src/js/standalone.js: -------------------------------------------------------------------------------- 1 | // This file is used to create a standalone javascript file for use in a script tag. The 2 | // file that is output assumes that Video.js is available at `window.videojs`. 3 | 4 | require('./index')(undefined, window.SILVERMINE_VIDEOJS_CHROMECAST_CONFIG); 5 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.js" 4 | ], 5 | "extension": [ 6 | ".js" 7 | ], 8 | "reporter": [ 9 | "text-summary", 10 | "html", 11 | "lcov" 12 | ], 13 | "instrument": true, 14 | "sourceMap": true, 15 | "all": true 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # build directory 12 | dist 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | # IDE 32 | **/.idea 33 | 34 | # VIM 35 | .*.sw? 36 | 37 | # OS 38 | .DS_Store 39 | .tmp 40 | -------------------------------------------------------------------------------- /tests/ChromcastButton.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('expect.js'); 4 | 5 | const chromecastButton = require('../src/js/components/ChromecastButton'); 6 | 7 | class ButtonComponentStub {} 8 | 9 | describe('ChromecastButton', function() { 10 | it('should not call videojs.extend', function() { 11 | const videoJsSpy = { 12 | extend: function() { 13 | expect().fail('videojs.extend is deprecated'); 14 | }, 15 | getComponent: function() { 16 | return ButtonComponentStub; 17 | }, 18 | registerComponent: function(_, component) { 19 | expect(component.prototype instanceof ButtonComponentStub).to.be(true); 20 | }, 21 | }; 22 | 23 | chromecastButton(videoJsSpy); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Jeremy Thomerson 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | silvermine-videojs-chromecast Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Demo of silvermine-videojs-chromecast

14 | 15 | 18 | 19 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | var preloadWebComponents = require('./preloadWebComponents'), 3 | createChromecastButton = require('./components/ChromecastButton'), 4 | createChromecastTech = require('./tech/ChromecastTech'), 5 | enableChromecast = require('./enableChromecast'); 6 | 7 | /** 8 | * @module index 9 | */ 10 | 11 | /** 12 | * Registers the Chromecast plugin and ChromecastButton Component with Video.js. See 13 | * {@link module:ChromecastButton} and {@link module:enableChromecast} for more details 14 | * about how the plugin and button are registered and configured. 15 | * 16 | * @param videojs {object} the videojs library. If `undefined`, this plugin 17 | * will look to `window.videojs`. 18 | * @param userOpts {object} the options to use for configuration 19 | * @see module:enableChromecast 20 | * @see module:ChromecastButton 21 | */ 22 | module.exports = function(videojs, userOpts) { 23 | var options = Object.assign({ preloadWebComponents: false }, userOpts); 24 | 25 | if (options.preloadWebComponents) { 26 | preloadWebComponents(); 27 | } 28 | 29 | videojs = videojs || window.videojs; 30 | createChromecastButton(videojs); 31 | createChromecastTech(videojs); 32 | enableChromecast(videojs); 33 | }; 34 | -------------------------------------------------------------------------------- /src/scss/_chromecastButton.scss: -------------------------------------------------------------------------------- 1 | // Images 2 | $icon-chromecast--default: 'images/ic_cast_white_24dp.png' !default; 3 | $icon-chromecast--hover: 'images/ic_cast_white_24dp.png' !default; 4 | $icon-chromecast-casting: 'images/ic_cast_connected_white_24dp.png' !default; 5 | $icon-chromecast-casting--hover: 'images/ic_cast_connected_white_24dp.png' !default; 6 | 7 | // Sizes 8 | $chromecast-icon-size: 12px !default; 9 | $chromecast-button-spacing: 4px !default; 10 | 11 | .vjs-chromecast-button { 12 | .vjs-icon-placeholder { 13 | background: url($icon-chromecast--default) center center no-repeat; 14 | background-size: contain; 15 | display: inline-block; 16 | width: $chromecast-icon-size; 17 | height: $chromecast-icon-size; 18 | } 19 | &:hover { 20 | cursor: pointer; 21 | .vjs-icon-placeholder { 22 | background-image: url($icon-chromecast--hover); 23 | } 24 | } 25 | &.vjs-chromecast-casting-state { 26 | .vjs-icon-placeholder { 27 | background-image: url($icon-chromecast-casting); 28 | } 29 | &:hover .vjs-icon-placeholder { 30 | background-image: url($icon-chromecast-casting--hover); 31 | } 32 | } 33 | } 34 | 35 | .vjs-chromecast-button.vjs-chromecast-button-lg:not(.vjs-hidden) { 36 | // Fits both the icon and the label on the same control 37 | display: flex; 38 | align-items: center; 39 | width: auto; 40 | padding: 0 $chromecast-button-spacing; 41 | .vjs-chromecast-button-label { 42 | flex-grow: 1; 43 | margin-left: $chromecast-button-spacing; 44 | } 45 | .vjs-icon-placeholder { 46 | flex-grow: 1; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/js/preloadWebComponents.js: -------------------------------------------------------------------------------- 1 | function doesUserAgentContainString(str) { 2 | return typeof window.navigator.userAgent === 'string' && window.navigator.userAgent.indexOf(str) >= 0; 3 | } 4 | 5 | // For information as to why this is needed, please see: 6 | // https://github.com/silvermine/videojs-chromecast/issues/17 7 | // https://github.com/silvermine/videojs-chromecast/issues/22 8 | 9 | module.exports = function() { 10 | var needsWebComponents = !document.registerElement, 11 | iosChrome = doesUserAgentContainString('CriOS'), 12 | androidChrome; 13 | 14 | androidChrome = doesUserAgentContainString('Android') 15 | && doesUserAgentContainString('Chrome/') 16 | && window.navigator.presentation; 17 | 18 | // These checks are based on the checks found in `cast_sender.js` which 19 | // determine if `cast_framework.js` needs to be loaded 20 | if ((androidChrome || iosChrome) && needsWebComponents) { 21 | // This is requiring webcomponents.js@0.7.24 because that's what was used 22 | // by the Chromecast framework at the time this was added. 23 | // We are using webcomponents-lite.js because it doesn't interfere with jQuery as 24 | // badly (e.g. it doesn't interfere with jQuery's fix for consistently bubbling 25 | // events, see #21). While the "lite" version does not include the shadow DOM 26 | // polyfills that the Chromecast framework may need for the 27 | // component to work properly, this plugin does not use the 28 | // component. 29 | require('webcomponents.js/webcomponents-lite.js'); // eslint-disable-line global-require 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/scss/_tech.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $chromecast-color-main: #cccccc !default; 3 | 4 | // Sizes 5 | $chromecast-title-font-size: 22px !default; 6 | $chromecast-subtitle-font-size: 18px !default; 7 | $chromecast-poster-width: 100px !default; 8 | $chromecast-poster-max-height: 180px !default; 9 | 10 | .vjs-tech-chromecast { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | overflow: hidden; 16 | .vjs-tech-chromecast-poster { 17 | &::after { 18 | content: ' '; 19 | display: block; 20 | height: 2px; 21 | width: $chromecast-poster-width; 22 | background-color: $chromecast-color-main; 23 | position: absolute; 24 | left: calc(50% - #{$chromecast-poster-width * 0.5}); 25 | } 26 | } 27 | .vjs-tech-chromecast-poster-img { 28 | max-height: $chromecast-poster-max-height; 29 | width: auto; 30 | border: 2px solid $chromecast-color-main; 31 | &.vjs-tech-chromecast-poster-img-empty { 32 | width: 160px; 33 | height: 90px; 34 | } 35 | } 36 | .vjs-tech-chromecast-title-container { 37 | position: absolute; 38 | bottom: 50%; 39 | margin-bottom: 100px; 40 | color: $chromecast-color-main; 41 | text-align: center; 42 | } 43 | .vjs-tech-chromecast-title { 44 | font-size: $chromecast-title-font-size; 45 | &.vjs-tech-chromecast-title-empty { 46 | display: none; 47 | } 48 | } 49 | .vjs-tech-chromecast-subtitle { 50 | font-size: $chromecast-subtitle-font-size; 51 | padding-top: 0.5em; 52 | &.vjs-tech-chromecast-subtitle-empty { 53 | display: none; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [our coding standards][commit-messages] for commit guidelines. 5 | 6 | ## [1.5.0](https://github.com/silvermine/videojs-chromecast/compare/v1.4.1...v1.5.0) (2023-11-07) 7 | 8 | 9 | ### Features 10 | 11 | * Allow modifying the load request ([#123](https://github.com/silvermine/videojs-chromecast/issues/123), [#141](https://github.com/silvermine/videojs-chromecast/issues/141)) ([7cee052](https://github.com/silvermine/videojs-chromecast/commit/7cee052dcd5473448f882d67bb5bc9d8e9a1763c)) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * Clear the close session timeout after new source starts playing ([4a8eb31](https://github.com/silvermine/videojs-chromecast/commit/4a8eb31faa241235c54c1f8dec897571360e7f19)) 17 | 18 | 19 | ### [1.4.1](https://github.com/silvermine/videojs-chromecast/compare/v1.4.0...v1.4.1) (2023-03-21) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * remove deprecated `.extend` method ([994b5b9](https://github.com/silvermine/videojs-chromecast/commit/994b5b9ae6df89f657e2ff4a920056826094b54f)), closes [#152](https://github.com/silvermine/videojs-chromecast/issues/152) [#147](https://github.com/silvermine/videojs-chromecast/issues/147) 25 | 26 | 27 | ## [1.4.0](https://github.com/silvermine/videojs-chromecast/compare/v1.3.4...v1.4.0) (2023-03-21) 28 | 29 | 30 | ### Features 31 | 32 | * Add optional "Cast" label to button component ([220c362](https://github.com/silvermine/videojs-chromecast/commit/220c36247c9ac992b757b97257e24e665ac3feb5)) 33 | * Change label to "Disconnect Cast" when connected ([a7f495b](https://github.com/silvermine/videojs-chromecast/commit/a7f495b78d8472322079a75a834440fd20858b3c)) 34 | * Set image on cast device if the video has a poster ([902464c](https://github.com/silvermine/videojs-chromecast/commit/902464cecf468c554fb062bd93d09bae9e303922)) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * Chromecast button with label styling breaking on cast/disconnect ([2a181de](https://github.com/silvermine/videojs-chromecast/commit/2a181dea847927050b57453f9474a2f47028fdca)) 40 | 41 | 42 | [commit-messages]: https://github.com/silvermine/silvermine-info/blob/master/commit-history.md#commit-messages 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | nvmrc: ${{ steps.makeNodeVersionOutput.outputs.nvmrc }} 10 | steps: 11 | - 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 # Fetch all history 15 | - 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version-file: '.nvmrc' 19 | - 20 | name: Put NVM version in output 21 | id: makeNodeVersionOutput 22 | run: echo "nvmrc=$(cat .nvmrc)" >> "$GITHUB_OUTPUT" 23 | - run: npm ci 24 | - run: npm run check-node-version 25 | - run: npm run standards 26 | - run: npm run build 27 | - 28 | name: Check for uncommitted changes # Done after dependency install and build to ensure code isn't compromised 29 | run: if [ -n "$(git status --porcelain)" ]; then echo 'There are uncommitted changes.'; exit 1; fi 30 | test: 31 | needs: [ build ] 32 | runs-on: ubuntu-latest 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | node-version: [ 16, '${{ needs.build.outputs.nvmrc }}', 'lts/*', 'latest' ] 37 | steps: 38 | - 39 | uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 # Fetch all history 42 | - 43 | name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | - run: npm ci # Reinstall the dependencies to ensure they install with the current version of node 48 | - run: npm run standards 49 | - run: npm run build # Ensure building is possible with this version of node 50 | - run: npm test 51 | - 52 | name: Coveralls 53 | uses: coverallsapp/github-action@v2 54 | with: 55 | parallel: true 56 | flag-name: ${{ matrix.node-version }} 57 | finish: 58 | needs: [ test ] 59 | runs-on: ubuntu-latest 60 | steps: 61 | - 62 | name: Close parallel build 63 | uses: coverallsapp/github-action@v2 64 | with: 65 | parallel-finished: true 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@silvermine/videojs-chromecast", 3 | "version": "1.5.0", 4 | "description": "video.js plugin for casting to chromecast", 5 | "main": "src/js/index.js", 6 | "scripts": { 7 | "check-node-version": "check-node-version --node $(cat .nvmrc) --npm 10.5.0 --print", 8 | "commitlint": "commitlint --from 5ed6165", 9 | "test": "nyc mocha -- 'tests/**/*.test.js'", 10 | "build": "grunt build", 11 | "build:debug": "grunt build --debug", 12 | "stylelint": "stylelint './src/scss/**/*.scss'", 13 | "eslint": "eslint '{,!(node_modules|dist)/**/}*.js'", 14 | "markdownlint": "markdownlint-cli2", 15 | "standards": "npm run commitlint && npm run markdownlint && npm run stylelint && npm run eslint", 16 | "release:preview": "node ./node_modules/@silvermine/standardization/scripts/release.js preview", 17 | "release:prep-changelog": "node ./node_modules/@silvermine/standardization/scripts/release.js prep-changelog", 18 | "release:finalize": "node ./node_modules/@silvermine/standardization/scripts/release.js finalize", 19 | "prepublish": "npm run build" 20 | }, 21 | "author": "Jeremy Thomerson", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/silvermine/videojs-chromecast.git" 26 | }, 27 | "keywords": [ 28 | "video.js", 29 | "videojs", 30 | "plugin", 31 | "google", 32 | "chromecast", 33 | "cast" 34 | ], 35 | "bugs": { 36 | "url": "https://github.com/silvermine/videojs-chromecast/issues" 37 | }, 38 | "homepage": "https://github.com/silvermine/videojs-chromecast#readme", 39 | "dependencies": { 40 | "webcomponents.js": "git+https://git@github.com/webcomponents/webcomponentsjs.git#v0.7.24" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "7.20.7", 44 | "@babel/preset-env": "7.20.2", 45 | "@commitlint/cli": "8.3.5", 46 | "@commitlint/travis-cli": "8.3.5", 47 | "@silvermine/eslint-config": "3.0.1", 48 | "@silvermine/standardization": "2.2.0", 49 | "autoprefixer": "8.6.5", 50 | "babel-eslint": "10.1.0", 51 | "babelify": "10.0.0", 52 | "check-node-version": "4.0.3", 53 | "core-js": "3.11.0", 54 | "coveralls": "3.0.2", 55 | "eslint": "6.8.0", 56 | "expect.js": "0.3.1", 57 | "grunt": "1.4.0", 58 | "grunt-browserify": "5.3.0", 59 | "grunt-contrib-clean": "2.0.0", 60 | "grunt-contrib-copy": "1.0.0", 61 | "grunt-contrib-uglify": "5.2.2", 62 | "grunt-contrib-watch": "1.1.0", 63 | "grunt-postcss": "0.9.0", 64 | "grunt-sass": "3.1.0", 65 | "mocha": "8.4.0", 66 | "mocha-lcov-reporter": "1.3.0", 67 | "nyc": "15.1.0", 68 | "rewire": "2.5.2", 69 | "sass": "1.52.3", 70 | "silvermine-serverless-utils": "git+https://github.com/silvermine/serverless-utils.git#910f1149af824fc8d0fa840878079c7d3df0f414", 71 | "sinon": "2.3.5" 72 | }, 73 | "peerDependencies": { 74 | "video.js": ">= 6 < 9" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/ChromcastTech.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('expect.js'); 4 | 5 | const sinon = require('sinon'); 6 | 7 | const chromecastTech = require('../src/js/tech/ChromecastTech'); 8 | 9 | class TechComponentStub { 10 | constructor() { 11 | this._ui = { 12 | getPoster: () => {}, 13 | updatePoster: () => {}, 14 | updateSubtitle: () => {}, 15 | updateTitle: () => {}, 16 | }; 17 | this.trigger = () => {}; 18 | } 19 | on() {} 20 | ready() {} 21 | } 22 | 23 | describe('ChromecastTech', function() { 24 | let originalCast, 25 | originalChrome; 26 | 27 | this.beforeEach(() => { 28 | originalCast = global.cast; 29 | global.cast = { 30 | framework: { 31 | RemotePlayerEventType: {}, 32 | }, 33 | }; 34 | 35 | originalChrome = global.chrome; 36 | global.chrome = { 37 | cast: { 38 | media: { 39 | GenericMediaMetadata: function() {}, 40 | LoadRequest: function() {}, 41 | MediaInfo: function() {}, 42 | MetadataType: {}, 43 | StreamType: {}, 44 | }, 45 | }, 46 | }; 47 | }); 48 | 49 | this.afterEach(() => { 50 | global.cast = originalCast; 51 | global.chrome = originalChrome; 52 | }); 53 | 54 | it('should not call videojs.extend', function() { 55 | const videoJsSpy = { 56 | extend: function() { 57 | expect().fail('videojs.extend is deprecated'); 58 | }, 59 | getComponent: function() { 60 | return TechComponentStub; 61 | }, 62 | registerTech: function(_, component) { 63 | expect(component.prototype instanceof TechComponentStub).to.be(true); 64 | }, 65 | }; 66 | 67 | chromecastTech(videoJsSpy); 68 | }); 69 | 70 | it('should call castSession.loadMedia with accurate req from chrome.cast.media.LoadRequest', function() { 71 | let ChromecastTech; 72 | 73 | const loadMediaSpy = sinon.stub().returns(Promise.resolve()); 74 | 75 | const videoJsSpy = () => { 76 | return { 77 | chromecastSessionManager: { 78 | getCastContext: () => { 79 | return { 80 | getCurrentSession: () => { 81 | return { 82 | loadMedia: loadMediaSpy, 83 | }; 84 | }, 85 | }; 86 | }, 87 | getRemotePlayer: () => {}, 88 | getRemotePlayerController: () => { 89 | return { 90 | addEventListener: () => {}, 91 | }; 92 | }, 93 | }, 94 | poster: () => {}, 95 | }; 96 | }; 97 | 98 | videoJsSpy.getComponent = () => { 99 | return TechComponentStub; 100 | }; 101 | videoJsSpy.registerTech = (_, component) => { 102 | ChromecastTech = component; 103 | }; 104 | 105 | const fakeRequest = {}; 106 | 107 | sinon.stub(global.chrome.cast.media, 'LoadRequest').returns(fakeRequest); 108 | 109 | chromecastTech(videoJsSpy); 110 | 111 | // eslint-disable-next-line no-new 112 | new ChromecastTech({ 113 | source: 'source.url', 114 | }); 115 | 116 | expect(loadMediaSpy.calledWith(fakeRequest)).to.be(true); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /src/js/tech/ChromecastTechUI.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class represents the UI that is shown in the player while the Chromecast Tech is 3 | * active. The UI has a single root DOM element that displays the poster image of the 4 | * current item and title and subtitle. This class receives updates to the poster, title 5 | * and subtitle when the media item that the player is playing changes. 6 | * 7 | * @class ChromecastTechUI 8 | */ 9 | class ChromecastTechUI { 10 | constructor() { 11 | this._el = this._createDOMElement(); 12 | } 13 | 14 | /** 15 | * Creates and returns a single DOMElement that contains the UI. This implementation 16 | * of the Chromecast Tech's UI displays a poster image, a title and a subtitle. 17 | * 18 | * @private 19 | * @returns {DOMElement} 20 | */ 21 | _createDOMElement() { 22 | var el = this._createElement('div', 'vjs-tech vjs-tech-chromecast'), 23 | posterContainerEl = this._createElement('div', 'vjs-tech-chromecast-poster'), 24 | posterImageEl = this._createElement('img', 'vjs-tech-chromecast-poster-img'), 25 | titleEl = this._createElement('div', 'vjs-tech-chromecast-title'), 26 | subtitleEl = this._createElement('div', 'vjs-tech-chromecast-subtitle'), 27 | titleContainer = this._createElement('div', 'vjs-tech-chromecast-title-container'); 28 | 29 | posterContainerEl.appendChild(posterImageEl); 30 | titleContainer.appendChild(titleEl); 31 | titleContainer.appendChild(subtitleEl); 32 | 33 | el.appendChild(titleContainer); 34 | el.appendChild(posterContainerEl); 35 | 36 | return el; 37 | } 38 | 39 | /** 40 | * A helper method for creating DOMElements of the given type and with the given class 41 | * name(s). 42 | * 43 | * @param type {string} the kind of DOMElement to create (ex: 'div') 44 | * @param className {string} the class name(s) to give to the DOMElement. May also be 45 | * a space-delimited list of class names. 46 | * @returns {DOMElement} 47 | */ 48 | _createElement(type, className) { 49 | var el = document.createElement(type); 50 | 51 | el.className = className; 52 | return el; 53 | } 54 | 55 | /** 56 | * Gets the root DOMElement to be shown in the player's UI. 57 | * 58 | * @returns {DOMElement} 59 | */ 60 | getDOMElement() { 61 | return this._el; 62 | } 63 | 64 | /** 65 | * Finds the poster's DOMElement in the root UI element. 66 | * 67 | * @private 68 | * @returns {DOMElement} 69 | */ 70 | _findPosterEl() { 71 | return this._el.querySelector('.vjs-tech-chromecast-poster'); 72 | } 73 | 74 | /** 75 | * Finds the poster's DOMElement in the root UI element. 76 | * 77 | * @private 78 | * @returns {DOMElement} 79 | */ 80 | _findPosterImageEl() { 81 | return this._el.querySelector('.vjs-tech-chromecast-poster-img'); 82 | } 83 | 84 | /** 85 | * Finds the title's DOMElement in the root UI element. 86 | * 87 | * @private 88 | * @returns {DOMElement} 89 | */ 90 | _findTitleEl() { 91 | return this._el.querySelector('.vjs-tech-chromecast-title'); 92 | } 93 | 94 | /** 95 | * Finds the subtitle's DOMElement in the root UI element. 96 | * 97 | * @private 98 | * @returns {DOMElement} 99 | */ 100 | _findSubtitleEl() { 101 | return this._el.querySelector('.vjs-tech-chromecast-subtitle'); 102 | } 103 | 104 | /** 105 | * Sets the current poster image URL and updates the poster image DOMElement with the 106 | * new poster image URL. 107 | * 108 | * @param poster {string} a URL for a poster image 109 | */ 110 | updatePoster(poster) { 111 | var posterImageEl = this._findPosterImageEl(); 112 | 113 | this._poster = poster ? poster : null; 114 | if (poster) { 115 | posterImageEl.setAttribute('src', poster); 116 | posterImageEl.classList.remove('vjs-tech-chromecast-poster-img-empty'); 117 | } else { 118 | posterImageEl.removeAttribute('src'); 119 | posterImageEl.classList.add('vjs-tech-chromecast-poster-img-empty'); 120 | } 121 | } 122 | 123 | /** 124 | * Gets the current poster image URL. 125 | * 126 | * @returns {string} the URL for th current poster image 127 | */ 128 | getPoster() { 129 | return this._poster; 130 | } 131 | 132 | /** 133 | * Sets the current title and updates the title's DOMElement with the new text. 134 | * 135 | * @param title {string} a title to show 136 | */ 137 | updateTitle(title) { 138 | var titleEl = this._findTitleEl(); 139 | 140 | this._title = title; 141 | if (title) { 142 | titleEl.innerHTML = title; 143 | titleEl.classList.remove('vjs-tech-chromecast-title-empty'); 144 | } else { 145 | titleEl.classList.add('vjs-tech-chromecast-title-empty'); 146 | } 147 | } 148 | 149 | /** 150 | * Sets the current subtitle and updates the subtitle's DOMElement with the new text. 151 | * 152 | * @param subtitle {string} a subtitle to show 153 | */ 154 | updateSubtitle(subtitle) { 155 | var subtitleEl = this._findSubtitleEl(); 156 | 157 | this._subtitle = subtitle; 158 | if (subtitle) { 159 | subtitleEl.innerHTML = subtitle; 160 | subtitleEl.classList.remove('vjs-tech-chromecast-subtitle-empty'); 161 | } else { 162 | subtitleEl.classList.add('vjs-tech-chromecast-subtitle-empty'); 163 | } 164 | } 165 | } 166 | 167 | module.exports = ChromecastTechUI; 168 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 Jeremy Thomerson 3 | * Licensed under the MIT license. 4 | */ 5 | 'use strict'; 6 | 7 | var path = require('path'), 8 | getCodeVersion = require('silvermine-serverless-utils/src/get-code-version'), 9 | join = path.join.bind(path), 10 | sass = require('sass'); 11 | 12 | module.exports = function(grunt) { 13 | 14 | var DEBUG = !!grunt.option('debug'), 15 | config; 16 | 17 | config = { 18 | js: { 19 | all: [ 'Gruntfile.js', 'src/**/*.js', 'tests/**/*.js' ], 20 | browserMainFile: join('src', 'js', 'standalone.js'), 21 | }, 22 | 23 | sass: { 24 | all: [ '**/*.scss', '!**/node_modules/**/*' ], 25 | main: join('src', 'scss', 'videojs-chromecast.scss'), 26 | }, 27 | 28 | images: { 29 | base: join('src', 'images'), 30 | }, 31 | 32 | dist: { 33 | base: join(__dirname, 'dist'), 34 | }, 35 | }; 36 | 37 | config.dist.js = { 38 | bundle: join(config.dist.base, 'silvermine-videojs-chromecast.js'), 39 | minified: join(config.dist.base, 'silvermine-videojs-chromecast.min.js'), 40 | }; 41 | 42 | config.dist.css = { 43 | base: config.dist.base, 44 | main: join(config.dist.base, 'silvermine-videojs-chromecast.css'), 45 | }; 46 | 47 | config.dist.images = join(config.dist.base, 'images'); 48 | 49 | grunt.initConfig({ 50 | 51 | pkg: grunt.file.readJSON('package.json'), 52 | versionInfo: getCodeVersion.both(), 53 | config: config, 54 | 55 | clean: { 56 | build: [ config.dist.base ], 57 | }, 58 | 59 | copy: { 60 | images: { 61 | files: [ 62 | { 63 | expand: true, 64 | cwd: config.images.base, 65 | src: '**/*', 66 | dest: config.dist.images, 67 | }, 68 | ], 69 | }, 70 | }, 71 | 72 | browserify: { 73 | main: { 74 | src: config.js.browserMainFile, 75 | dest: config.dist.js.bundle, 76 | options: { 77 | transform: [ 78 | [ 79 | 'babelify', 80 | { 81 | presets: [ 82 | [ 83 | '@babel/preset-env', 84 | { 85 | debug: DEBUG, 86 | useBuiltIns: 'usage', 87 | shippedProposals: true, 88 | corejs: 3, 89 | }, 90 | ], 91 | ], 92 | }, 93 | ], 94 | ], 95 | }, 96 | }, 97 | }, 98 | 99 | uglify: { 100 | main: { 101 | files: { 102 | '<%= config.dist.js.minified %>': config.dist.js.bundle, 103 | }, 104 | options: { 105 | banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> <%= versionInfo %> */\n', 106 | sourceMap: DEBUG, 107 | sourceMapIncludeSources: DEBUG, 108 | mangle: !DEBUG, 109 | // Disable the `merge_vars` option in the compression phase. 110 | // `merge_vars` aggressively reuses variable names, which can lead to 111 | // unexpected behavior or runtime errors in certain cases. 112 | compress: DEBUG ? false : { merge_vars: false }, // eslint-disable-line camelcase 113 | beautify: DEBUG, 114 | }, 115 | }, 116 | }, 117 | 118 | sass: { 119 | main: { 120 | files: [ 121 | { 122 | src: config.sass.main, 123 | dest: config.dist.css.main, 124 | ext: '.css', 125 | extDot: 'first', 126 | }, 127 | ], 128 | }, 129 | options: { 130 | implementation: sass, 131 | sourceMap: DEBUG, 132 | indentWidth: 3, 133 | outputStyle: DEBUG ? 'expanded' : 'compressed', 134 | sourceComments: DEBUG, 135 | }, 136 | }, 137 | 138 | postcss: { 139 | options: { 140 | map: DEBUG, 141 | processors: [ 142 | require('autoprefixer')(), // eslint-disable-line global-require 143 | ], 144 | }, 145 | styles: { 146 | src: config.dist.css.main, 147 | }, 148 | }, 149 | 150 | watch: { 151 | grunt: { 152 | files: [ 'Gruntfile.js' ], 153 | tasks: [ 'build' ], 154 | }, 155 | 156 | js: { 157 | files: [ 'src/**/*.js' ], 158 | tasks: [ 'build-js' ], 159 | }, 160 | 161 | css: { 162 | files: [ 'src/**/*.scss' ], 163 | tasks: [ 'build-css' ], 164 | }, 165 | }, 166 | 167 | }); 168 | 169 | grunt.loadNpmTasks('grunt-contrib-clean'); 170 | grunt.loadNpmTasks('grunt-contrib-uglify'); 171 | grunt.loadNpmTasks('grunt-browserify'); 172 | grunt.loadNpmTasks('grunt-contrib-copy'); 173 | grunt.loadNpmTasks('grunt-contrib-watch'); 174 | grunt.loadNpmTasks('grunt-sass'); 175 | grunt.loadNpmTasks('grunt-postcss'); 176 | 177 | grunt.registerTask('build-js', [ 'browserify', 'uglify' ]); 178 | grunt.registerTask('build-css', [ 'sass', 'postcss:styles' ]); 179 | grunt.registerTask('build', [ 'build-js', 'build-css', 'copy:images' ]); 180 | grunt.registerTask('develop', [ 'build', 'watch' ]); 181 | grunt.registerTask('default', [ 'build' ]); 182 | 183 | }; 184 | -------------------------------------------------------------------------------- /src/js/components/ChromecastButton.js: -------------------------------------------------------------------------------- 1 | module.exports = function(videojs) { 2 | 3 | /** 4 | * Registers the ChromecastButton Component with Video.js. Calls 5 | * {@link http://docs.videojs.com/Component.html#.registerComponent}, which will add a 6 | * component called `chromecastButton` to the list of globally registered Video.js 7 | * components. The `chromecastButton` is added to the player's control bar UI 8 | * automatically once {@link module:enableChromecast} has been called. If you would 9 | * like to specify the order of the buttons that appear in the control bar, including 10 | * this button, you can do so in the options that you pass to the `videojs` function 11 | * when creating a player: 12 | * 13 | * ``` 14 | * videojs('playerID', { 15 | * controlBar: { 16 | * children: [ 17 | * 'playToggle', 18 | * 'progressControl', 19 | * 'volumePanel', 20 | * 'fullscreenToggle', 21 | * 'chromecastButton', 22 | * ], 23 | * } 24 | * }); 25 | * ``` 26 | * 27 | * @param videojs {object} A reference to {@link http://docs.videojs.com/module-videojs.html|Video.js} 28 | * @see http://docs.videojs.com/module-videojs.html#~registerPlugin 29 | */ 30 | 31 | /** 32 | * The Video.js Button class is the base class for UI button components. 33 | * 34 | * @external Button 35 | * @see {@link http://docs.videojs.com/Button.html|Button} 36 | */ 37 | const ButtonComponent = videojs.getComponent('Button'); 38 | 39 | /** 40 | * The ChromecastButton module contains both the ChromecastButton class definition and 41 | * the function used to register the button as a Video.js Component. 42 | * @module ChromecastButton 43 | */ 44 | 45 | 46 | /** @lends ChromecastButton.prototype **/ 47 | class ChromecastButton extends ButtonComponent { 48 | 49 | /** 50 | * This class is a button component designed to be displayed in the 51 | * player UI's control bar. It opens the Chromecast menu when clicked. 52 | * 53 | * @constructs 54 | * @extends external:Button 55 | * @param player {Player} the video.js player instance 56 | */ 57 | constructor(player, options) { 58 | super(player, options); 59 | 60 | player.on('chromecastConnected', this._onChromecastConnected.bind(this)); 61 | player.on('chromecastDisconnected', this._onChromecastDisconnected.bind(this)); 62 | player.on('chromecastDevicesAvailable', this._onChromecastDevicesAvailable.bind(this)); 63 | player.on('chromecastDevicesUnavailable', this._onChromecastDevicesUnavailable.bind(this)); 64 | 65 | // Use the initial state of `hasAvailableDevices` to call the corresponding event 66 | // handlers because the corresponding events may have already been emitted before 67 | // binding the listeners above. 68 | if (player.chromecastSessionManager && player.chromecastSessionManager.hasAvailableDevices()) { 69 | this._onChromecastDevicesAvailable(); 70 | } else { 71 | this._onChromecastDevicesUnavailable(); 72 | } 73 | 74 | if (options.addCastLabelToButton) { 75 | this.el().classList.add('vjs-chromecast-button-lg'); 76 | 77 | this._labelEl = document.createElement('span'); 78 | this._labelEl.classList.add('vjs-chromecast-button-label'); 79 | this._updateCastLabelText(); 80 | 81 | this.el().appendChild(this._labelEl); 82 | } else { 83 | this.controlText('Open Chromecast menu'); 84 | } 85 | } 86 | 87 | /** 88 | * Overrides Button#buildCSSClass to return the classes used on the button element. 89 | * 90 | * @param el {DOMElement} 91 | * @see {@link http://docs.videojs.com/Button.html#buildCSSClass|Button#buildCSSClass} 92 | */ 93 | buildCSSClass() { 94 | return 'vjs-chromecast-button ' + 95 | (this._isChromecastConnected ? 'vjs-chromecast-casting-state ' : '') + 96 | (this.options_.addCastLabelToButton ? 'vjs-chromecast-button-lg ' : '') + 97 | ButtonComponent.prototype.buildCSSClass(); 98 | } 99 | 100 | /** 101 | * Overrides Button#handleClick to handle button click events. Chromecast 102 | * functionality is handled outside of this class, which should be limited 103 | * to UI related logic. This function simply triggers an event on the player. 104 | * 105 | * @fires ChromecastButton#chromecastRequested 106 | * @param el {DOMElement} 107 | * @see {@link http://docs.videojs.com/Button.html#handleClick|Button#handleClick} 108 | */ 109 | handleClick() { 110 | this.player().trigger('chromecastRequested'); 111 | } 112 | 113 | /** 114 | * Handles `chromecastConnected` player events. 115 | * 116 | * @private 117 | */ 118 | _onChromecastConnected() { 119 | this._isChromecastConnected = true; 120 | this._reloadCSSClasses(); 121 | this._updateCastLabelText(); 122 | } 123 | 124 | /** 125 | * Handles `chromecastDisconnected` player events. 126 | * 127 | * @private 128 | */ 129 | _onChromecastDisconnected() { 130 | this._isChromecastConnected = false; 131 | this._reloadCSSClasses(); 132 | this._updateCastLabelText(); 133 | } 134 | 135 | /** 136 | * Handles `chromecastDevicesAvailable` player events. 137 | * 138 | * @private 139 | */ 140 | _onChromecastDevicesAvailable() { 141 | this.show(); 142 | } 143 | 144 | /** 145 | * Handles `chromecastDevicesUnavailable` player events. 146 | * 147 | * @private 148 | */ 149 | _onChromecastDevicesUnavailable() { 150 | this.hide(); 151 | } 152 | 153 | /** 154 | * Re-calculates which CSS classes the button needs and sets them on the buttons' 155 | * DOMElement. 156 | * 157 | * @private 158 | */ 159 | _reloadCSSClasses() { 160 | if (!this.el_) { 161 | return; 162 | } 163 | this.el_.className = this.buildCSSClass(); 164 | } 165 | 166 | /** 167 | * Updates the optional cast label text based on whether the chromecast is connected 168 | * or disconnected. 169 | * 170 | * @private 171 | */ 172 | _updateCastLabelText() { 173 | if (!this._labelEl) { 174 | return; 175 | } 176 | this._labelEl.textContent = this._isChromecastConnected ? this.localize('Disconnect Cast') : this.localize('Cast'); 177 | } 178 | } 179 | 180 | videojs.registerComponent('chromecastButton', ChromecastButton); 181 | }; 182 | -------------------------------------------------------------------------------- /src/js/enableChromecast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module enableChromecast 3 | */ 4 | 5 | var ChromecastSessionManager = require('./chromecast/ChromecastSessionManager'), 6 | CHECK_AVAILABILITY_INTERVAL = 1000, // milliseconds 7 | CHECK_AVAILABILITY_TIMEOUT = 30 * 1000; // milliseconds 8 | 9 | 10 | /** 11 | * Configures the Chromecast 12 | * [casting context](https://developers.google.com/cast/docs/reference/chrome/cast.framework.CastContext), 13 | * which is required before casting. 14 | * 15 | * @private 16 | * @param options {object} the plugin options 17 | */ 18 | function configureCastContext(options) { 19 | var context = cast.framework.CastContext.getInstance(); 20 | 21 | context.setOptions({ 22 | receiverApplicationId: options.receiverAppID || chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, 23 | // Setting autoJoinPolicy to ORIGIN_SCOPED prevents this plugin from automatically 24 | // trying to connect to a preexisting Chromecast session, if one exists. The user 25 | // must end any existing session before trying to cast from this player instance. 26 | autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, 27 | }); 28 | } 29 | 30 | /** 31 | * Handles the `chromecastRequested` event. Delegates to a `chromecastSessionManager` 32 | * instance. 33 | * 34 | * @private 35 | * @param player {object} a Video.js player instance 36 | */ 37 | function onChromecastRequested(player) { 38 | player.chromecastSessionManager.openCastMenu(); 39 | } 40 | 41 | /** 42 | * Adds the Chromecast button to the player's control bar, if one does not already exist, 43 | * then starts listening for the `chromecastRequested` event. 44 | * 45 | * @private 46 | * @param player {object} a Video.js player instance 47 | * @param options {object} the plugin options 48 | */ 49 | function setUpChromecastButton(player, options) { 50 | var indexOpt; 51 | 52 | // Ensure Chromecast button exists 53 | if (options.addButtonToControlBar && !player.controlBar.getChild('chromecastButton')) { 54 | // Figure out Chromecast button's index 55 | indexOpt = player.controlBar.children().length; 56 | if (typeof options.buttonPositionIndex !== 'undefined') { 57 | indexOpt = options.buttonPositionIndex >= 0 58 | ? options.buttonPositionIndex 59 | : player.controlBar.children().length + options.buttonPositionIndex; 60 | } 61 | player.controlBar.addChild('chromecastButton', options, indexOpt); 62 | } 63 | // Respond to requests for casting. The ChromecastButton component triggers this event 64 | // when the user clicks the Chromecast button. 65 | player.on('chromecastRequested', onChromecastRequested.bind(null, player)); 66 | } 67 | 68 | /** 69 | * Creates a {@link ChromecastSessionManager} and assigns it to the player. 70 | * 71 | * @private 72 | * @param player {object} a Video.js player instance 73 | */ 74 | function createSessionManager(player) { 75 | if (!player.chromecastSessionManager) { 76 | player.chromecastSessionManager = new ChromecastSessionManager(player); 77 | } 78 | } 79 | 80 | /** 81 | * Sets up and configures the casting context and Chromecast button. 82 | * 83 | * @private 84 | * @param options {object} the plugin options 85 | */ 86 | function enableChromecast(player, options) { 87 | configureCastContext(options); 88 | createSessionManager(player); 89 | setUpChromecastButton(player, options); 90 | } 91 | 92 | /** 93 | * Waits for the Chromecast APIs to become available, then configures the casting context 94 | * and configures the Chromecast button. The Chromecast APIs are loaded asynchronously, 95 | * so we must wait until they are available before initializing the casting context and 96 | * Chromecast button. 97 | * 98 | * @private 99 | * @param player {object} a Video.js player instance 100 | * @param options {object} the plugin options 101 | */ 102 | function waitUntilChromecastAPIsAreAvailable(player, options) { 103 | var maxTries = CHECK_AVAILABILITY_TIMEOUT / CHECK_AVAILABILITY_INTERVAL, 104 | tries = 1, 105 | intervalID; 106 | 107 | // The Chromecast APIs are loaded asynchronously, so they may not be loaded and 108 | // initialized at this point. The Chromecast APIs do provide a callback function that 109 | // is called after the framework has loaded, but it requires you to define the callback 110 | // function **before** loading the APIs. That would require us to expose some callback 111 | // function to `window` here, and would require users of this plugin to define a 112 | // Chromecast API callback on `window` that calls our callback function in their HTML 113 | // file. To avoid all of this, we simply check to see if the Chromecast API is 114 | // available periodically, and stop after a timeout threshold has passed. 115 | // 116 | // See https://developers.google.com/cast/docs/chrome_sender_integrate#initialization 117 | intervalID = setInterval(function() { 118 | if (tries > maxTries) { 119 | clearInterval(intervalID); 120 | return; 121 | } 122 | if (ChromecastSessionManager.isChromecastAPIAvailable()) { 123 | clearInterval(intervalID); 124 | enableChromecast(player, options); 125 | } 126 | tries = tries + 1; 127 | }, CHECK_AVAILABILITY_INTERVAL); 128 | 129 | } 130 | 131 | /** 132 | * Registers the Chromecast plugin with Video.js. Calls 133 | * [videojs#registerPlugin](http://docs.videojs.com/module-videojs.html#~registerPlugin), 134 | * which will add a plugin function called `chromecast` to any instance of a Video.js 135 | * player that is created after calling this function. Call `player.chromecast(options)`, 136 | * passing in configuration options, to enable the Chromecast plugin on your Player 137 | * instance. 138 | * 139 | * Currently, there are only two configuration options: 140 | * 141 | * * **`receiverAppID`** - the string ID of a [Chromecast receiver 142 | * app](https://developers.google.com/cast/docs/receiver_apps) to use. Defaults to 143 | * the [default Media Receiver 144 | * ID](https://developers.google.com/cast/docs/receiver_apps#default). 145 | * * **`addButtonToControlBar`** - flag that tells the plugin 146 | * whether or not it should automatically add the Chromecast button the the Video.js 147 | * player's control bar component. Defaults to `true`. 148 | * 149 | * Other configuration options are set through the player's Chromecast Tech configuration: 150 | * 151 | * ``` 152 | * var playerOptions, player, pluginOptions; 153 | * 154 | * playerOptions = { 155 | * chromecast: { 156 | * requestTitleFn: function(source) { 157 | * return titles[source.url]; 158 | * }, 159 | * requestSubtitleFn: function(source) { 160 | * return subtitles[source.url]; 161 | * }, 162 | * requestCustomDataFn: function(source) { 163 | * return customData[source.url]; 164 | * } 165 | * } 166 | * }; 167 | * 168 | * pluginOptions = { 169 | * receiverAppID: '1234', 170 | * addButtonToControlBar: false, 171 | * }; 172 | * 173 | * player = videojs(document.getElementById('myVideoElement'), playerOptions); 174 | * player.chromecast(pluginOptions); // initializes the Chromecast plugin 175 | * ``` 176 | * 177 | * @param {object} videojs 178 | * @see http://docs.videojs.com/module-videojs.html#~registerPlugin 179 | */ 180 | module.exports = function(videojs) { 181 | videojs.registerPlugin('chromecast', function(options) { 182 | var pluginOptions = Object.assign({ addButtonToControlBar: true }, options || {}); 183 | 184 | // `this` is an instance of a Video.js Player. 185 | // Wait until the player is "ready" so that the player's control bar component has 186 | // been created. 187 | this.ready(function() { 188 | if (!this.controlBar) { 189 | return; 190 | } 191 | if (ChromecastSessionManager.isChromecastAPIAvailable()) { 192 | enableChromecast(this, pluginOptions); 193 | } else { 194 | waitUntilChromecastAPIsAreAvailable(this, pluginOptions); 195 | } 196 | }.bind(this)); 197 | }); 198 | }; 199 | -------------------------------------------------------------------------------- /src/js/chromecast/ChromecastSessionManager.js: -------------------------------------------------------------------------------- 1 | /** @lends ChromecastSessionManager.prototype **/ 2 | class ChromecastSessionManager { 3 | 4 | /** 5 | * Stores the state of the current Chromecast session and its associated objects such 6 | * as the 7 | * [RemotePlayerController](https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayerController), 8 | * and the 9 | * [RemotePlayer](https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayer). 10 | * 11 | * WARNING: Do not instantiate this class until the 12 | * [CastContext](https://developers.google.com/cast/docs/reference/chrome/cast.framework.CastContext) 13 | * has been configured. 14 | * 15 | * For an undocumented (and thus unknown) reason, RemotePlayer and 16 | * RemotePlayerController instances created before the cast context has been configured 17 | * or after requesting a session or loading media will not stay in sync with media 18 | * items that are loaded later. 19 | * 20 | * For example, the first item that you cast will work as expected: events on 21 | * RemotePlayerController will fire and the state (currentTime, duration, etc) of the 22 | * RemotePlayer instance will update as the media item plays. However, if a new media 23 | * item is loaded via a `loadMedia` request, the media item will play, but the 24 | * remotePlayer will be in a "media unloaded" state where the duration is 0, the 25 | * currentTime does not update, and no change events are fired (except, strangely, 26 | * displayStatus updates). 27 | * 28 | * @param player {object} Video.js Player 29 | * @constructs ChromecastSessionManager 30 | */ 31 | constructor(player) { 32 | this.player = player; 33 | 34 | this._sessionListener = this._onSessionStateChange.bind(this); 35 | this._castListener = this._onCastStateChange.bind(this); 36 | 37 | this._addCastContextEventListeners(); 38 | 39 | // Remove global event listeners when this player instance is destroyed to prevent 40 | // memory leaks. 41 | this.player.on('dispose', this._removeCastContextEventListeners.bind(this)); 42 | 43 | this._notifyPlayerOfDevicesAvailabilityChange(this.getCastContext().getCastState()); 44 | 45 | this.remotePlayer = new cast.framework.RemotePlayer(); 46 | this.remotePlayerController = new cast.framework.RemotePlayerController(this.remotePlayer); 47 | } 48 | 49 | static hasConnected = false; 50 | 51 | /** 52 | * Add event listeners for events triggered on the current CastContext. 53 | * 54 | * @private 55 | */ 56 | _addCastContextEventListeners() { 57 | var sessionStateChangedEvt = cast.framework.CastContextEventType.SESSION_STATE_CHANGED, 58 | castStateChangedEvt = cast.framework.CastContextEventType.CAST_STATE_CHANGED; 59 | 60 | this.getCastContext().addEventListener(sessionStateChangedEvt, this._sessionListener); 61 | this.getCastContext().addEventListener(castStateChangedEvt, this._castListener); 62 | } 63 | 64 | /** 65 | * Remove event listeners that were added in {@link 66 | * ChromecastSessionManager#_addCastContextEventListeners}. 67 | * 68 | * @private 69 | */ 70 | _removeCastContextEventListeners() { 71 | var sessionStateChangedEvt = cast.framework.CastContextEventType.SESSION_STATE_CHANGED, 72 | castStateChangedEvt = cast.framework.CastContextEventType.CAST_STATE_CHANGED; 73 | 74 | this.getCastContext().removeEventListener(sessionStateChangedEvt, this._sessionListener); 75 | this.getCastContext().removeEventListener(castStateChangedEvt, this._castListener); 76 | } 77 | 78 | /** 79 | * Handle the CastContext's SessionState change event. 80 | * 81 | * @private 82 | */ 83 | _onSessionStateChange(event) { 84 | if (event.sessionState === cast.framework.SessionState.SESSION_ENDED) { 85 | this.player.trigger('chromecastDisconnected'); 86 | this._reloadTech(); 87 | } 88 | } 89 | 90 | /** 91 | * Handle the CastContext's CastState change event. 92 | * 93 | * @private 94 | */ 95 | _onCastStateChange(event) { 96 | this._notifyPlayerOfDevicesAvailabilityChange(event.castState); 97 | } 98 | 99 | /** 100 | * Triggers player events that notifies listeners that Chromecast devices are 101 | * either available or unavailable. 102 | * 103 | * @private 104 | */ 105 | _notifyPlayerOfDevicesAvailabilityChange(castState) { 106 | if (this.hasAvailableDevices(castState)) { 107 | this.player.trigger('chromecastDevicesAvailable'); 108 | } else { 109 | this.player.trigger('chromecastDevicesUnavailable'); 110 | } 111 | } 112 | 113 | /** 114 | * Returns whether or not there are Chromecast devices available to cast to. 115 | * 116 | * @see https://developers.google.com/cast/docs/reference/chrome/cast.framework#.CastState 117 | * @param {String} castState 118 | * @return {boolean} true if there are Chromecast devices available to cast to. 119 | */ 120 | hasAvailableDevices(castState) { 121 | castState = castState || this.getCastContext().getCastState(); 122 | 123 | return castState === cast.framework.CastState.NOT_CONNECTED || 124 | castState === cast.framework.CastState.CONNECTING || 125 | castState === cast.framework.CastState.CONNECTED; 126 | } 127 | 128 | /** 129 | * Opens the Chromecast casting menu by requesting a CastSession. Does nothing if the 130 | * Video.js player does not have a source. 131 | */ 132 | openCastMenu() { 133 | var onSessionSuccess; 134 | 135 | if (!this.player.currentSource()) { 136 | // Do not cast if there is no media item loaded in the player 137 | return; 138 | } 139 | onSessionSuccess = function() { 140 | ChromecastSessionManager.hasConnected = true; 141 | this.player.trigger('chromecastConnected'); 142 | this._reloadTech(); 143 | }.bind(this); 144 | 145 | // It is the `requestSession` function call that actually causes the cast menu to 146 | // open. 147 | // The second parameter to `.then` is an error handler. We use a noop function here 148 | // because we handle errors in the ChromecastTech class and we do not want an 149 | // error to bubble up to the console. This error handler is also triggered when 150 | // the user closes out of the chromecast selector pop-up without choosing a 151 | // casting destination. 152 | this.getCastContext().requestSession() 153 | .then(onSessionSuccess, function() { /* noop */ }); 154 | } 155 | 156 | /** 157 | * Reloads the Video.js player's Tech. This causes the player to re-evaluate which 158 | * Tech should be used for the current source by iterating over available Tech and 159 | * calling `Tech.isSupported` and `Tech.canPlaySource`. Video.js uses the first 160 | * Tech that returns true from both of those functions. This is what allows us to 161 | * switch back and forth between the Chromecast Tech and other available Tech when a 162 | * CastSession is connected or disconnected. 163 | * 164 | * @private 165 | */ 166 | _reloadTech() { 167 | var player = this.player, 168 | currentTime = player.currentTime(), 169 | wasPaused = player.paused(), 170 | sources = player.currentSources(); 171 | 172 | // Reload the current source(s) to re-lookup and use the currently available Tech. 173 | // The chromecast Tech gets used if `ChromecastSessionManager.isChromecastConnected` 174 | // is true (effectively, if a chromecast session is currently in progress), 175 | // otherwise Video.js continues to search through the Tech list for other eligible 176 | // Tech to use, such as the HTML5 player. 177 | player.src(sources); 178 | 179 | player.ready(function() { 180 | if (wasPaused) { 181 | player.pause(); 182 | } else { 183 | player.play(); 184 | } 185 | player.currentTime(currentTime || 0); 186 | }); 187 | } 188 | 189 | /** 190 | * @see https://developers.google.com/cast/docs/reference/chrome/cast.framework.CastContext 191 | * @returns {object} the current CastContext, if one exists 192 | */ 193 | 194 | getCastContext() { 195 | return cast.framework.CastContext.getInstance(); 196 | } 197 | 198 | /** 199 | * @see https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayer 200 | * @returns {object} the current RemotePlayer, if one exists 201 | */ 202 | getRemotePlayer() { 203 | return this.remotePlayer; 204 | } 205 | 206 | /** 207 | * @see https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayerController 208 | * @returns {object} the current RemotePlayerController, if one exists 209 | */ 210 | getRemotePlayerController() { 211 | return this.remotePlayerController; 212 | } 213 | 214 | /** 215 | * Returns whether or not the current Chromecast API is available (that is, 216 | * `window.chrome`, `window.chrome.cast`, and `window.cast` exist). 217 | * 218 | * @static 219 | * @returns {boolean} true if the Chromecast API is available 220 | */ 221 | static isChromecastAPIAvailable() { 222 | return window.chrome && window.chrome.cast && window.cast; 223 | } 224 | 225 | /** 226 | * Returns whether or not there is a current CastSession and it is connected. 227 | * 228 | * @static 229 | * @returns {boolean} true if the current CastSession exists and is connected 230 | */ 231 | static isChromecastConnected() { 232 | // We must also check the `hasConnected` flag because 233 | // `getCastContext().getCastState()` returns `CONNECTED` even when the current 234 | // casting session was initiated by another tab in the browser or by another process 235 | return ChromecastSessionManager.isChromecastAPIAvailable() && 236 | (cast.framework.CastContext.getInstance().getCastState() === cast.framework.CastState.CONNECTED) && 237 | ChromecastSessionManager.hasConnected; 238 | } 239 | } 240 | 241 | module.exports = ChromecastSessionManager; 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silvermine VideoJS Chromecast Plugin 2 | 3 | 4 | [![Build Status](https://travis-ci.org/silvermine/videojs-chromecast.svg?branch=master)](https://travis-ci.org/silvermine/videojs-chromecast) 5 | [![Coverage Status](https://coveralls.io/repos/github/silvermine/videojs-chromecast/badge.svg?branch=master)](https://coveralls.io/github/silvermine/videojs-chromecast?branch=master) 6 | [![Dependency Status](https://david-dm.org/silvermine/videojs-chromecast.svg)](https://david-dm.org/silvermine/videojs-chromecast) 7 | [![Dev Dependency Status](https://david-dm.org/silvermine/videojs-chromecast/dev-status.svg)](https://david-dm.org/silvermine/videojs-chromecast#info=devDependencies&view=table) 8 | 9 | 10 | 11 | ## What is it? 12 | 13 | A plugin for [videojs](http://videojs.com/) versions 6+ that adds a button to the control 14 | bar which will cast videos to a Chromecast. 15 | 16 | 17 | ## How do I use it? 18 | 19 | The `@silvermine/videojs-chromecast` plugin includes 3 types of assets: JavaScript, CSS, 20 | and images. 21 | 22 | You can either build the plugin locally and use the assets that are output from the build 23 | process directly, or you can install the plugin as an NPM module, include the 24 | JavaScript and SCSS source in your project using a Common-JS module loader and SASS build 25 | process, and copy the images from the image source folder to your project. 26 | 27 | Note that regardless of whether you are using this plugin via the pre-built JS or as a 28 | module, the Chromecast framework will need to be included after the plugin. For example: 29 | 30 | ```html 31 | 32 | 33 | 34 | ``` 35 | 36 | ### Building the plugin locally 37 | 38 | 1. Either clone this repository or install the `@silvermine/videojs-chromecast` module 39 | using `npm install @silvermine/videojs-chromecast`. 40 | 2. Ensure that `@silvermine/videojs-chromecast`'s `devDependencies` are installed by 41 | running `npm install` from within the `videojs-chromecast` folder. 42 | 3. Run `grunt build` to build and copy the JavaScript, CSS and image files to the 43 | `videojs-chromecast/dist` folder. 44 | 4. Copy the plugin's files from the `dist` folder into your project as needed. 45 | 5. Ensure that the images in the `dist/images` folder are accessible at `./images/`, 46 | relative to where the plugin's CSS is located. If, for example, your CSS is located 47 | at `https://example.com/plugins/silvermine-videojs-chromecast.css`, then the 48 | plugin's images should be located at `https://example.com/plugins/images/`. 49 | 6. Follow the steps in the "Configuration" section below. 50 | 51 | Note: when adding the plugin's JavaScript to your web page, include the 52 | `silvermine-videojs-chromecast.min.js` JavaScript file in your HTML *after* loading 53 | Video.js. The plugin's built JavaScript file expects there to be a reference to Video.js 54 | at `window.videojs` and will throw an error if it does not exist. 55 | 56 | ### Initialization options 57 | 58 | * **`preloadWebComponents`** (default: `false`) - The Chromecast framework relies on 59 | the `webcomponents.js` polyfill when a browser does not have 60 | `document.registerElement` in order to create the `` custom 61 | component (which is not used by this plugin). If you are using jQuery, this polyfill 62 | must be loaded and initialized before jQuery is initialized. Unfortunately, the 63 | Chromecast framework loads the `webcomponents.js` polyfill via a dynamically created 64 | ` 105 | 106 | ``` 107 | 108 | ### Configuration 109 | 110 | Once the plugin has been loaded and registered, configure it and add it to your Video.js 111 | player using Video.js' plugin configuration option (see the section under the heading 112 | "Setting up a Plugin" on [Video.js' plugin documentation page][videojs-docs]. 113 | 114 | **Important: In addition to defining plugin configuration, you are required to define the 115 | player's `techOrder` option, setting `'chromecast'` as the first Tech in the list.** Below 116 | is an example of the minimum required configuration for the Chromecast plugin to function: 117 | 118 | ```js 119 | var options; 120 | 121 | options = { 122 | controls: true, 123 | techOrder: [ 'chromecast', 'html5' ], // You may have more Tech, such as Flash or HLS 124 | plugins: { 125 | chromecast: {} 126 | } 127 | }; 128 | 129 | videojs(document.getElementById('myVideoElement'), options); 130 | ``` 131 | 132 | Please note that even if you choose not to use any of the configuration options, you must 133 | either provide a `chromecast` entry in the `plugins` option for Video.js to initialize the 134 | plugin for you: 135 | 136 | ```js 137 | options = { 138 | plugins: { 139 | chromecast: {} 140 | } 141 | }; 142 | ``` 143 | 144 | or you must initialize the plugin manually: 145 | 146 | ```js 147 | var player = videojs(document.getElementById('myVideoElement')); 148 | 149 | player.chromecast(); // initializes the Chromecast plugin 150 | ``` 151 | 152 | #### Configuration options 153 | 154 | ##### Plugin configuration 155 | 156 | * **`plugins.chromecast.receiverAppID`** - the string ID of a custom [Chromecast 157 | receiver app][cast-receiver] to use. Defaults to the [default Media Receiver 158 | ID][def-cast-id]. 159 | * **`plugins.chromecast.addButtonToControlBar`** - a `boolean` flag that tells the 160 | plugin whether or not it should automatically add the Chromecast button to the 161 | Video.js player's control bar component. Defaults to `true`. 162 | * **`plugins.chromecast.buttonPositionIndex`** - a zero-based number specifying the 163 | index of the Chromecast button among the control bar's child components (if 164 | `addButtonToControlBar` is set to `true`). By default the Chromecast Button is added 165 | as the last child of the control bar. A value less than 0 puts the button at the 166 | specified position from the end of the control bar. Note that it's likely not all 167 | child components of the control bar are visible. 168 | * **`plugins.chromecast.addCastLabelToButton`** (default: `false`) - by default, the 169 | Chromecast button component will display only an icon. Setting `addCastLabelToButton` 170 | to `true` will display a label titled `"Cast"` alongside the default icon. 171 | 172 | ##### Chromecast Tech configuration 173 | 174 | * **`chromecast.requestTitleFn`** - a function that this plugin calls when it needs a 175 | string that will be the title shown in the UI that is shown when a Chromecast session 176 | is active and connected. When the this plugin calls the `requestTitleFn`, it passes 177 | it the [current `source` object][player-source] and expects a string in return. If 178 | nothing is returned or if this option is not defined, no title will be shown. 179 | * **`chromecast.requestSubtitleFn`** - a function that this plugin calls when it needs 180 | a string that will be the sub-title shown in the UI that is shown when a Chromecast 181 | session is active and connected. When the this plugin calls the `requestSubtitleFn`, 182 | it passes it the [current `source` object][player-source] and expects a string in 183 | return. If nothing is returned or if this option is not defined, no sub-title will be 184 | shown. 185 | * **`chromecast.requestCustomDataFn`** - a function that this plugin calls when it 186 | needs an object that contains custom information necessary for a Chromecast receiver 187 | app when a session is active and connected. When the this plugin calls the 188 | `requestCustomDataFn`, it passes it the [current `source` object][player-source] and 189 | expects an object in return. If nothing is returned or if this option is not defined, 190 | no custom data will be sent. This option is intended to be used with a [custom 191 | receiver][custom-receiver] application to extend its default capabilities. 192 | * **`chromecast.modifyLoadRequestFn`** - a function that this plugin calls before doing 193 | the request to [load media][chromecast-load-media]. The function gets called with 194 | the [LoadRequest][chromecast-load-request] object as argument and expects it in 195 | return. 196 | 197 | Here is an example configuration object that makes full use of all required and optional 198 | configuration: 199 | 200 | ```js 201 | var titles, subtitles, customData, options; 202 | 203 | titles = { 204 | 'https://example.com/videos/video-1.mp4': 'Example Title', 205 | 'https://example.com/videos/video-2.mp4': 'Example Title2', 206 | }; 207 | 208 | subtitles = { 209 | 'https://example.com/videos/video-1.mp4': 'Subtitle', 210 | 'https://example.com/videos/video-2.mp4': 'Subtitle2', 211 | }; 212 | 213 | customData = { 214 | 'https://example.com/videos/video-1.mp4': { 'customColor': '#0099ee' }, 215 | 'https://example.com/videos/video-2.mp4': { 'customColor': '#000080' }, 216 | }; 217 | 218 | options = { 219 | // Must specify the 'chromecast' Tech first 220 | techOrder: [ 'chromecast', 'html5' ], // Required 221 | // Configuration for the Chromecast Tech 222 | chromecast: { 223 | requestTitleFn: function(source) { // Not required 224 | return titles[source.url]; 225 | }, 226 | requestSubtitleFn: function(source) { // Not required 227 | return subtitles[source.url]; 228 | }, 229 | requestCustomDataFn: function(source) { // Not required 230 | return customData[source.url]; 231 | }, 232 | modifyLoadRequestFn: function (loadRequest) { // HLS support 233 | loadRequest.media.hlsSegmentFormat = 'fmp4'; 234 | loadRequest.media.hlsVideoSegmentFormat = 'fmp4'; 235 | return loadRequest; 236 | } 237 | }, 238 | plugins: { 239 | chromecast: { 240 | receiverAppID: '1234' // Not required 241 | addButtonToControlBar: false, // Defaults to true 242 | }, 243 | } 244 | }; 245 | ``` 246 | 247 | ##### Localization 248 | 249 | The `ChromecastButton` component has two translated strings: "Open Chromecast menu" and 250 | "Cast". 251 | 252 | * The "Open Chromecast menu" string appears in both of the standard places for Button 253 | component accessibility text: inside the `.vjs-control-text` span and as the 254 | `