├── .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 | [](https://travis-ci.org/silvermine/videojs-chromecast)
5 | [](https://coveralls.io/github/silvermine/videojs-chromecast?branch=master)
6 | [](https://david-dm.org/silvermine/videojs-chromecast)
7 | [](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 | `