├── app ├── .gitkeep ├── components │ ├── .gitkeep │ └── ember-youtube.js └── templates │ └── components │ ├── .gitkeep │ └── ember-youtube.hbs ├── addon ├── .gitkeep └── components │ └── ember-youtube.js ├── vendor └── .gitkeep ├── tests ├── helpers │ └── .gitkeep ├── unit │ └── .gitkeep ├── integration │ ├── .gitkeep │ └── components │ │ └── ember-youtube-test.js ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── video.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ ├── application.js │ │ │ └── index.js │ │ ├── templates │ │ │ ├── components │ │ │ │ └── .gitkeep │ │ │ ├── video.hbs │ │ │ ├── application.hbs │ │ │ └── index.hbs │ │ ├── resolver.js │ │ ├── router.js │ │ ├── app.js │ │ ├── styles │ │ │ └── app.css │ │ └── index.html │ ├── public │ │ └── robots.txt │ └── config │ │ ├── targets.js │ │ └── environment.js ├── test-helper.js ├── acceptance │ └── video-test.js └── index.html ├── .watchmanconfig ├── index.js ├── config ├── environment.js └── ember-try.js ├── .ember-cli ├── .npmignore ├── .gitignore ├── .editorconfig ├── ember-cli-build.js ├── testem.js ├── .eslintrc.js ├── LICENSE.md ├── CHANGELOG.md ├── .travis.yml ├── package.json └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'ember-youtube' 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default Controller.extend({}); 4 | -------------------------------------------------------------------------------- /app/components/ember-youtube.js: -------------------------------------------------------------------------------- 1 | import EmberYoutube from 'ember-youtube/components/ember-youtube'; 2 | 3 | export default EmberYoutube; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/video.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | 3 |

Video route. Playing {{model.id}}.

4 | 5 | {{ember-youtube ytid=model.id}} 6 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Demonstration of the ember-youtube component. See the repository for more information.

2 | 3 | 7 | 8 |
9 | 10 | {{outlet}} 11 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | this.route('video', {path: 'video/:youtube_id'}); 11 | }); 12 | 13 | export default Router; 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .eslintrc.js 11 | .gitignore 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | 18 | # ember-try 19 | .node_modules.ember-try/ 20 | bower.json.ember-try 21 | package.json.ember-try 22 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/video.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import Object from '@ember/object'; 3 | import RSVP from 'rsvp' 4 | 5 | export default Route.extend({ 6 | model(params) { 7 | const model = Object.create({ 8 | id: params.youtube_id 9 | }); 10 | // Fake a network request. 11 | return new RSVP.Promise(resolve => { 12 | setTimeout(() => resolve(model), 500) 13 | }) 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | yarn-error.log 18 | testem.log 19 | 20 | # ember-try 21 | .node_modules.ember-try/ 22 | bower.json.ember-try 23 | package.json.ember-try 24 | -------------------------------------------------------------------------------- /tests/acceptance/video-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { visit, currentURL } from '@ember/test-helpers'; 3 | import { setupApplicationTest } from 'ember-qunit'; 4 | 5 | module('Acceptance | video', function(hooks) { 6 | setupApplicationTest(hooks); 7 | 8 | test('visiting /video', async function(assert) { 9 | await visit('/video/jmCytJPqQis'); 10 | 11 | assert.equal(currentURL(), '/video/jmCytJPqQis'); 12 | }); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | indent_style = tab 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [{package.json,*.yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.hbs] 20 | insert_final_newline = false 21 | 22 | [*.{diff,md}] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Georgia; 3 | } 4 | 5 | body { 6 | margin: 1rem; 7 | } 8 | 9 | section { 10 | display: block; 11 | padding: 1rem; 12 | margin-bottom: 1rem; 13 | } 14 | 15 | a { 16 | color: red; 17 | } 18 | 19 | a.active { 20 | background-color: red; 21 | color: white; 22 | } 23 | 24 | h1 { 25 | font-size: 1em; 26 | } 27 | 28 | h1 a { 29 | color: black; 30 | text-decoration: none; 31 | } 32 | 33 | .EmberYoutube-player { 34 | width: 480px; 35 | max-width: 100%; 36 | } 37 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | mode: 'ci', 13 | args: [ 14 | // --no-sandbox is needed when running Chrome inside a container 15 | process.env.TRAVIS ? '--no-sandbox' : null, 16 | 17 | '--disable-gpu', 18 | '--headless', 19 | '--remote-debugging-port=0', 20 | '--window-size=1440,900' 21 | ].filter(Boolean) 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ember YouTube 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import {debug} from '@ember/debug'; 3 | 4 | export default Controller.extend({ 5 | youTubeId: 'fZ7MhTRmJ60', 6 | volume: 100, 7 | 8 | init() { 9 | this._super() 10 | 11 | // On this route we'll use some different settings. 12 | this.set('customPlayerVars', { 13 | autoplay: 1, 14 | rel: 0, // disable related videos 15 | showinfo: 0 // hide uploader info 16 | }) 17 | }, 18 | 19 | actions: { 20 | ytPlaying() { 21 | debug('on playing from controller'); 22 | }, 23 | ytPaused() { 24 | debug('on paused from controller'); 25 | }, 26 | ytEnded() { 27 | debug('on ended from controller'); 28 | // here you could load another video by changing the youTubeId 29 | }, 30 | ytBuffering() { 31 | debug('on buffering from controller'); 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | plugins: [ 8 | 'ember' 9 | ], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:ember/recommended' 13 | ], 14 | env: { 15 | browser: true 16 | }, 17 | rules: { 18 | 'ember/closure-actions': 'warn' 19 | }, 20 | overrides: [ 21 | // node files 22 | { 23 | files: [ 24 | 'index.js', 25 | 'testem.js', 26 | 'ember-cli-build.js', 27 | 'config/**/*.js', 28 | 'tests/dummy/config/**/*.js' 29 | ], 30 | excludedFiles: [ 31 | 'app/**', 32 | 'addon/**', 33 | 'tests/dummy/app/**' 34 | ], 35 | parserOptions: { 36 | sourceType: 'script', 37 | ecmaVersion: 2015 38 | }, 39 | env: { 40 | browser: false, 41 | node: true 42 | }, 43 | plugins: ['node'], 44 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 45 | // add your custom rules and overrides for node files here 46 | }) 47 | } 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /app/templates/components/ember-youtube.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{#if showExtras}} 4 |
5 | {{#if showControls}} 6 | 7 | 8 | 9 | 10 | {{/if}} 11 | 12 | {{#if showProgress}} 13 |

14 | 15 |

16 | {{/if}} 17 | 18 | {{#if showDebug}} 19 |

20 | 21 | ytid: {{ytid}}
22 | playerState: {{playerState}}
23 | isMuted: {{isMuted}}
24 | isPlaying: {{isPlaying}}
25 | currentTime: {{currentTime}}
26 | duration: {{duration}}
27 | volume: {{volume}}
28 |
29 |

30 | {{/if}} 31 |
32 | {{/if}} 33 | 34 | {{#if hasBlock}} 35 |
36 | {{yield}} 37 |
38 | {{/if}} 39 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dummy Tests 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | {{content-for "test-head-footer"}} 18 | 19 | 20 | {{content-for "body"}} 21 | {{content-for "test-body"}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{content-for "body-footer"}} 30 | {{content-for "test-body-footer"}} 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |

Custom controls

2 |

You can play, pause, mute and unmute the component from the surrounding controller. Try changing the values below:

3 | 4 |
5 | YouTube ID: {{input value=youTubeId}}
6 | Volume: {{input type="number" value=volume}}
7 |
8 | 11 | 14 | 17 |
18 | 19 |
20 | 21 |
22 |

The player

23 | {{ember-youtube 24 | ytid=youTubeId 25 | volume=volume 26 | playerVars=customPlayerVars 27 | showControls=false 28 | showProgress=true 29 | showDebug=true 30 | lazyload=false 31 | 32 | delegate=this 33 | delegate-as="emberYoutube" 34 | 35 | playing=(action "ytPlaying") 36 | paused=(action "ytPaused") 37 | ended=(action "ytEnded") 38 | }} 39 |
40 | 41 |

Multiple players on the same route are supported as well.

42 | {{ember-youtube ytid="NEFrNP-BLcI"}} 43 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for ember-youtube 2 | 3 | ### 0.9.5 4 | 5 | No longer requires jQuery. 6 | 7 | ### 0.9.0 8 | 9 | New: access the player instance through all youtube events 10 | Fixed async tests by upgrading to ember-cli 3.0.0 and waitFor. 11 | 12 | ### 0.7.0 13 | 14 | Adds a new `lazyload` option to the component (disabled by default). If you're loading one or more players on a page without a `ytid` to start with, this will make sure the API and iframes are not created before a the YouTube ID is there. 15 | 16 | ### 0.6.1 17 | 18 | Options that would be mapped to playerVars have been removed (fs and autoplay) and are now set through a playerVars object on the component. Also through a bit of async trickery, the tests now pass. 19 | 20 | ### 0.5.3 21 | 22 | Extracted custom timestamps from ember-youtube to slim the addon. Moment.js will no longer be included by default. See the readme for instructions for instructions on how to do it manually. 23 | 24 | ### 0.5.0 25 | 26 | Fixed issues with playback and volume not being properly 'set' and 'get'. Loading of the YouTube API should be a bit faster. 27 | 28 | ### 0.4.0 29 | 30 | Thanks to two contributors we have have full control over the volume in the player and it's possible to format the current time and duration of the video using Moment.js. 31 | 32 | ### 0.2.0 33 | 34 | Added an option for autoplay and disabled it by default. 35 | 36 | ### 0.0.1 37 | 38 | First version, published. Playback, controls and progress bar is working. 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "6" 7 | 8 | sudo: false 9 | dist: trusty 10 | 11 | addons: 12 | chrome: stable 13 | 14 | cache: 15 | yarn: true 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | matrix: 22 | # we recommend new addons test the current and previous LTS 23 | # as well as latest stable release (bonus points to beta/canary) 24 | - EMBER_TRY_SCENARIO=ember-lts-2.12 25 | - EMBER_TRY_SCENARIO=ember-lts-2.16 26 | - EMBER_TRY_SCENARIO=ember-lts-2.18 27 | - EMBER_TRY_SCENARIO=ember-lts-3.4 28 | - EMBER_TRY_SCENARIO=ember-lts-3.8 29 | - EMBER_TRY_SCENARIO=ember-release 30 | - EMBER_TRY_SCENARIO=ember-beta 31 | - EMBER_TRY_SCENARIO=ember-canary 32 | - EMBER_TRY_SCENARIO=ember-default 33 | 34 | matrix: 35 | fast_finish: true 36 | allow_failures: 37 | - env: EMBER_TRY_SCENARIO=ember-canary 38 | - env: EMBER_TRY_SCENARIO=ember-beta 39 | 40 | before_install: 41 | - curl -o- -L https://yarnpkg.com/install.sh | bash 42 | - export PATH=$HOME/.yarn/bin:$PATH 43 | 44 | install: 45 | - yarn install --no-lockfile --non-interactive 46 | 47 | script: 48 | - yarn lint:js 49 | # Usually, it's ok to finish the test scenario without reverting 50 | # to the addon's original dependency state, skipping "cleanup". 51 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO --skip-cleanup 52 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const getChannelURL = require("ember-source-channel-url"); 4 | 5 | module.exports = function() { 6 | return Promise.all([ 7 | getChannelURL("release"), 8 | getChannelURL("beta"), 9 | getChannelURL("canary") 10 | ]).then(urls => { 11 | return { 12 | useYarn: true, 13 | scenarios: [ 14 | { 15 | name: "ember-lts-2.12", 16 | npm: { 17 | devDependencies: { 18 | "ember-source": "~2.12.0" 19 | } 20 | } 21 | }, 22 | { 23 | name: "ember-lts-2.16", 24 | npm: { 25 | devDependencies: { 26 | "ember-source": "~2.16.0" 27 | } 28 | } 29 | }, 30 | { 31 | name: "ember-lts-2.18", 32 | npm: { 33 | devDependencies: { 34 | "ember-source": "~2.18.0" 35 | } 36 | } 37 | }, 38 | { 39 | name: "ember-lts-3.4", 40 | npm: { 41 | devDependencies: { 42 | "ember-source": "~3.4.0" 43 | } 44 | } 45 | }, 46 | { 47 | name: "ember-lts-3.8", 48 | npm: { 49 | devDependencies: { 50 | "ember-source": "~3.8.0" 51 | } 52 | } 53 | }, 54 | { 55 | name: "ember-release", 56 | npm: { 57 | devDependencies: { 58 | "ember-source": urls[0] 59 | } 60 | } 61 | }, 62 | { 63 | name: "ember-beta", 64 | npm: { 65 | devDependencies: { 66 | "ember-source": urls[1] 67 | } 68 | } 69 | }, 70 | { 71 | name: "ember-canary", 72 | npm: { 73 | devDependencies: { 74 | "ember-source": urls[2] 75 | } 76 | } 77 | }, 78 | { 79 | name: "ember-default", 80 | npm: { 81 | devDependencies: {} 82 | } 83 | } 84 | ] 85 | }; 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-youtube", 3 | "version": "0.9.7", 4 | "description": "A simple Ember.js component to play and control single YouTube videos using the iframe API.", 5 | "keywords": [ 6 | "ember-addon", 7 | "youtube", 8 | "video", 9 | "player" 10 | ], 11 | "license": "MIT", 12 | "author": "", 13 | "directories": { 14 | "doc": "doc", 15 | "test": "tests" 16 | }, 17 | "repository": "https://github.com/oskarrough/ember-youtube", 18 | "scripts": { 19 | "build": "ember build --environment=production", 20 | "lint:js": "eslint ./*.js addon addon-test-support app config lib server test-support tests", 21 | "start": "ember serve", 22 | "test": "ember try:each", 23 | "deploy-surge": "mv dist/index.html dist/200.html; surge dist ember-youtube.surge.sh" 24 | }, 25 | "dependencies": { 26 | "ember-cli-babel": "^6.6.0", 27 | "ember-concurrency": "^0.8.27 || ^0.9.0 || ^0.10.0 || ^1.0.0", 28 | "ember-cli-htmlbars": "^2.0.1", 29 | "ember-cli-htmlbars-inline-precompile": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "broccoli-asset-rev": "^2.4.5", 33 | "ember-cli": "~3.0.0", 34 | "ember-cli-dependency-checker": "^2.0.0", 35 | "ember-cli-eslint": "^4.2.1", 36 | "ember-cli-inject-live-reload": "^1.4.1", 37 | "ember-cli-qunit": "^4.1.1", 38 | "ember-cli-shims": "^1.2.0", 39 | "ember-cli-uglify": "^2.0.0", 40 | "ember-disable-prototype-extensions": "^1.1.2", 41 | "ember-export-application-global": "^2.0.0", 42 | "ember-load-initializers": "^1.0.0", 43 | "ember-maybe-import-regenerator": "^0.1.6", 44 | "ember-resolver": "^4.0.0", 45 | "ember-source": "~3.0.0", 46 | "ember-source-channel-url": "^1.0.1", 47 | "ember-try": "^0.2.23", 48 | "eslint-plugin-ember": "^5.0.0", 49 | "eslint-plugin-node": "^5.2.1", 50 | "loader.js": "^4.2.3" 51 | }, 52 | "engines": { 53 | "node": "^4.5 || 6.* || >= 7.*" 54 | }, 55 | "ember-addon": { 56 | "configPath": "tests/dummy/config", 57 | "demoURL": "https://ember-youtube.surge.sh" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/integration/components/ember-youtube-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit' 2 | import { setupRenderingTest } from 'ember-qunit' 3 | import { render, waitFor } from '@ember/test-helpers' 4 | import hbs from 'htmlbars-inline-precompile' 5 | 6 | // https://github.com/emberjs/ember-test-helpers/blob/master/API.md#wait-helpers 7 | 8 | module('Integration | Component | ember-youtube', function(hooks) { 9 | setupRenderingTest(hooks) 10 | 11 | test('youtube api replaces our container with an iframe', async function (assert) { 12 | this.set('youTubeId', 'w311Hd4K_Fk') 13 | await render(hbs`{{ember-youtube ytid=youTubeId}}`) 14 | assert.ok(this.element.querySelector('.EmberYoutube-player'), 'has container') 15 | await waitFor('iframe') 16 | assert.equal(this.element.querySelector('.EmberYoutube-player').tagName, 'IFRAME', 'container is replaced') 17 | }) 18 | 19 | test('it can show and remove controls', async function (assert) { 20 | assert.expect(3) 21 | this.set('controls', true) 22 | await render(hbs`{{ember-youtube showControls=controls}}`) 23 | assert.ok(this.element.querySelector('.EmberYoutube-controls button')) 24 | assert.equal(this.element.querySelectorAll('.EmberYoutube-controls button')[0].textContent, 'Play') 25 | this.set('controls', false) 26 | assert.notOk(this.element.querySelector('.EmberYoutube-controls button')) 27 | }) 28 | 29 | test('it can show and hide progress', async function (assert) { 30 | assert.expect(2) 31 | this.set('progress', false) 32 | await render(hbs`{{ember-youtube showProgress=progress}}`) 33 | assert.notOk(this.element.querySelector('.EmberYoutube-progress'), 'Progress is hidden by default') 34 | this.set('progress', true) 35 | assert.ok(this.element.querySelector('.EmberYoutube-progress'), 'Progress can be shown') 36 | }) 37 | 38 | // Todo: create a promise so we can assert autoplay is working 39 | // test('it can autoplay', async function (assert) { 40 | // assert.expect(2) 41 | 42 | // this.set('youTubeId', 'w311Hd4K_Fk') 43 | // this.set('myPlayerVars', {autoplay: 1}) 44 | 45 | // await render(hbs` 46 | // {{ember-youtube 47 | // ytid=youTubeId 48 | // showControls=true 49 | // playerVars=myPlayerVars}}`) 50 | 51 | // var buttons = this.element.querySelectorAll('.EmberYoutube-controls button') 52 | // var btn = buttons[0] 53 | // // console.log(btn) 54 | // assert.equal(btn.textContent, 'Play') 55 | // // assert.equal(btn.textContent, 'Pause', 'it says pause because it is already playing') 56 | // }) 57 | }) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-youtube 2 | 3 | An Ember.js component to play and control YouTube videos using the iframe API. Pass it a YouTube video ID and you're good to go! Every day this component is being used on [Radio4000](https://radio4000.com). 4 | 5 | You can see a demonstration at [ember-youtube.surge.sh](http://ember-youtube.surge.sh). 6 | 7 | ## Features 8 | 9 | - Full support for all YouTube player events (and errors) 10 | - Custom (external) controls (make your own buttons) 11 | - Custom progress bar in full sync with the YouTube player 12 | - Extra: custom time properties (for instance "4:31 / 7:58") formatted with Moment.js 13 | 14 | [![TravisCI Build Status][travis-badge]][travis-badge-url] 15 | 16 | [travis-badge]: https://travis-ci.org/oskarrough/ember-youtube.svg?branch=master 17 | [travis-badge-url]: https://travis-ci.org/oskarrough/ember-youtube 18 | 19 | 20 | ## Quick start 21 | 22 | Inside your Ember CLI project run: 23 | 24 | ```bash 25 | ember install ember-youtube 26 | ``` 27 | 28 | Use the component like this: 29 | 30 | ```hbs 31 | {{ember-youtube ytid="fZ7MhTRmJ60"}} 32 | ``` 33 | 34 | Here's another example with all options. Only `ytid` is required. 35 | 36 | ```hbs 37 | {{ember-youtube 38 | ytid="fZ7MhTRmJ60" 39 | volume=100 40 | playerVars=customPlayerVars 41 | showDebug=false 42 | showControls=false 43 | showProgress=false 44 | lazyload=false 45 | delegate=this 46 | delegate-as="emberYoutube" 47 | playing=(action "ytPlaying") 48 | paused=(action "ytPaused") 49 | ended=(action "ytEnded") 50 | buffering=(action "ytBuffering") 51 | }} 52 | ``` 53 | 54 | ## YouTube player options 55 | 56 | The YouTube API allows you to define an object of options called [playerVars](https://developers.google.com/youtube/player_parameters). With ember-youtube, you can optionally set this object on the component: 57 | 58 | ```javascript 59 | // controller.js 60 | myPlayerVars: { 61 | autoplay: 1, 62 | showinfo: 0, 63 | // Setting an origin can remove a YouTube 'postMessage' API warning in the console. 64 | // Note, this does not have any effect on localhost. 65 | origin: 'https://www.example.com' 66 | } 67 | ``` 68 | 69 | ```hbs 70 | {{ember-youtube ytid="fZ7MhTRmJ60" playerVars=myPlayerVars}} 71 | ``` 72 | 73 | ## External controls 74 | 75 | If you want your own buttons to control the player there are two steps. 76 | 77 | 1) Make the ember-youtube component available to the outside, which normally means your controller. You do this with the `delegate` and `delegate-as` properties of ember-youtube. They expose the component and give you a target for your button's actions. Like this: 78 | 79 | ```hbs 80 | {{ember-youtube ytid=youTubeId delegate=controller delegate-as="emberYoutube"}} 81 | ``` 82 | 83 | 2) Specify a target on your actions. Now, and because we used `delegate` and `delegate-as`, you'll have a `emberYoutube` property on your controller. This is where we'll target our actions. It allows you to do this in the template where you include the player: 84 | 85 | ```hbs 86 | {{ember-youtube ytid="fZ7MhTRmJ60" delegate=this delegate-as="emberYoutube"}} 87 | 90 | 93 | ``` 94 | 95 | You could also do this: 96 | 97 | ```hbs 98 | {{ember-youtube ytid="fZ7MhTRmJ60" delegate=this delegate-as="emberYoutube"}} 99 | 100 | 101 | 102 | 103 | ``` 104 | 105 | ## Seeking 106 | 107 | Here's an example of seeking to a certain timestamp in a video. It accepts a number of seconds. 108 | 109 | ```hbs 110 | 111 | {{ember-youtube ytid="fZ7MhTRmJ60" delegate=this delegate-as="emberYoutube"}} 112 | ``` 113 | 114 | ## Events 115 | 116 | The ember-youtube component send four different actions: `playing`, `paused`, `ended` and `buffering`. You should map them to your own actions like this: 117 | 118 | ```hbs 119 | {{ember-youtube ytid="fZ7MhTRmJ60" 120 | playing="ytPlaying" 121 | paused="ytPaused" 122 | ended="ytEnded" 123 | buffering="ytBuffering"}} 124 | ``` 125 | 126 | ```JavaScript 127 | actions: { 128 | ytPlaying(event) {}, 129 | ytPaused(event) {}, 130 | ytEnded(event) { 131 | // here you could load another video by changing the youTubeId 132 | }, 133 | ytBuffering(event) {} 134 | } 135 | ``` 136 | 137 | ## Lazy load 138 | 139 | Even if you don't supply an `ytid` to the ember-youtube component, it will make sure the iframe player is created as soon as possible. But if you set `lazyload=true`, it will wait for an `ytid`. This will, in some cases, improve the initial render performance. Example: 140 | 141 | ```hbs 142 | {{ember-youtube lazyload=true}} 143 | ``` 144 | 145 | ## Custom timestamps 146 | 147 | Let's write a component with two custom formatted timestamps such as "13:37". First make sure moment and moment-duration-format are installed. Then create a new component with the following template: 148 | 149 | ```hbs 150 | {{ember-youtube ytid=youTubeId delegate=this delegate-as="emberYoutube"}} 151 | 152 | // custom timestamp 153 |

154 | {{currentTimeFormatted}}/{{durationFormatted}} 155 |

156 | ``` 157 | 158 | And here's the JavaScript part of the component: 159 | 160 | ```javascript 161 | export default Ember.Component.extend({ 162 | currentTimeFormat: 'mm:ss', 163 | durationFormat: 'mm:ss', 164 | 165 | // returns a momentJS formated date based on "currentTimeFormat" property 166 | currentTimeFormatted: computed('emberYoutube.currentTime', 'currentTimeFormat', function () { 167 | let time = this.get('emberYoutube.currentTime'); 168 | let format = this.get('currentTimeFormat'); 169 | if (!time || !format) { 170 | return null; 171 | } 172 | let duration = moment.duration(time, 'seconds'); 173 | return duration.format(format); 174 | }), 175 | 176 | // returns a momentJS formated date based on "durationFormat" property 177 | durationFormatted: computed('emberYoutube.duration', 'durationFormat', function () { 178 | let duration = this.get('emberYoutube.duration'); 179 | let format = this.get('durationFormat'); 180 | if (!duration || !format) { 181 | return null; 182 | } 183 | let time = moment.duration(duration, 'seconds'); 184 | return time.format(format); 185 | }) 186 | }); 187 | ``` 188 | 189 | ## Autoplay on iOS 190 | 191 | On iOS autoplay of videos is disabled by Apple to save your precious data. I haven't been able to circumvent this. The user needs to tap the video itself before we can call the player's play/load methods. If anyone has a workaround, let me know. 192 | 193 | ## Development 194 | 195 | * `git clone` this repository 196 | * `yarn` 197 | * `ember server` 198 | * Visit your app at http://localhost:4200. 199 | 200 | ### Linting 201 | 202 | * `npm run lint:js` 203 | * `npm run lint:js -- --fix` 204 | 205 | ### Running tests 206 | 207 | * `ember test` – Runs the test suite on the current Ember version 208 | * `ember test --server` – Runs the test suite in "watch mode" 209 | * `npm test` – Runs `ember try:each` to test your addon against multiple Ember versions 210 | 211 | **Please file an issue if you have any feedback or would like to contribute.** 212 | 213 | Thanks to https://github.com/oskarrough/ember-youtube/graphs/contributors. 214 | 215 | This project is licensed under the [MIT License](LICENSE.md). 216 | -------------------------------------------------------------------------------- /addon/components/ember-youtube.js: -------------------------------------------------------------------------------- 1 | /* global YT, window */ 2 | 3 | import Component from '@ember/component'; 4 | import RSVP from 'rsvp' 5 | import { computed, getProperties, setProperties, observer } from '@ember/object'; 6 | import { debug } from '@ember/debug'; 7 | import { run } from '@ember/runloop'; 8 | import { task } from 'ember-concurrency'; 9 | 10 | export default Component.extend({ 11 | classNames: ['EmberYoutube'], 12 | ytid: null, 13 | width: 560, 14 | height: 315, 15 | 16 | // These options are used to load a video. 17 | startSeconds: undefined, 18 | endSeconds: undefined, 19 | suggestedQuality: undefined, 20 | 21 | lazyload: false, 22 | showControls: false, 23 | showDebug: false, 24 | showProgress: false, 25 | showExtras: computed.or('showControls', 'showProgress', 'showDebug'), 26 | 27 | player: null, 28 | playerState: 'loading', 29 | 30 | /* Hooks */ 31 | playerCreated() { 32 | /* Callback to be passed. */ 33 | }, 34 | playerStateChanged() { 35 | /* Callback to be passed. */ 36 | }, 37 | error() { 38 | /* Callback to be passed. */ 39 | }, 40 | 41 | /* State hooks */ 42 | ready() { 43 | /* Callback to be passed. */ 44 | }, 45 | ended() { 46 | /* Callback to be passed. */ 47 | }, 48 | playing() { 49 | /* Callback to be passed. */ 50 | }, 51 | paused() { 52 | /* Callback to be passed. */ 53 | }, 54 | buffering() { 55 | /* Callback to be passed. */ 56 | }, 57 | queued() { 58 | /* Callback to be passed. */ 59 | }, 60 | 61 | init() { 62 | this._super(); 63 | 64 | setProperties(this, { 65 | // YouTube's embedded player can take a number of optional parameters. 66 | // https://developers.google.com/youtube/player_parameters#Parameters 67 | // https://developers.google.com/youtube/youtube_player_demo 68 | playerVars: Object.assign({}, this.playerVars), 69 | // from YT.PlayerState 70 | stateNames: { 71 | '-1': 'ready', // READY 72 | 0: 'ended', // YT.Player.ENDED 73 | 1: 'playing', // YT.PlayerState.PLAYING 74 | 2: 'paused', // YT.PlayerState.PAUSED 75 | 3: 'buffering', // YT.PlayerState.BUFFERING 76 | 5: 'queued' // YT.PlayerState.CUED 77 | } 78 | }); 79 | 80 | this._register(); 81 | }, 82 | 83 | // Expose the component to the outside world. 84 | _register() { 85 | const delegate = this.get('delegate'); 86 | const delegateAs = this.get('delegate-as'); 87 | run.schedule('afterRender', () => { 88 | if (!delegate) { 89 | return; 90 | } 91 | delegate.set(delegateAs || 'emberYouTube', this); 92 | }); 93 | }, 94 | 95 | didInsertElement() { 96 | this._super(...arguments); 97 | 98 | this.addProgressBarClickHandler(); 99 | 100 | if (!this.get('lazyload') && this.get('ytid')) { 101 | // If "lazyload" is not enabled and we have an ID, we can start immediately. 102 | // Otherwise the `loadVideo` observer will take care of things. 103 | this.get('loadAndCreatePlayer').perform(); 104 | } 105 | }, 106 | 107 | willDestroyElement() { 108 | this.get('loadAndCreatePlayer').cancelAll(); 109 | 110 | // clear the timer 111 | this.stopTimer(); 112 | 113 | // remove progress bar click handler 114 | this.removeProgressBarClickHandler(); 115 | 116 | // destroy video player 117 | const player = this.get('player'); 118 | if (player) { 119 | player.destroy(); 120 | this.set('player', null); 121 | } 122 | 123 | // clear up if "delegated" 124 | const delegate = this.get('delegate'); 125 | const delegateAs = this.get('delegate-as'); 126 | if (delegate) { 127 | delegate.set(delegateAs || 'emberYouTube', null); 128 | } 129 | }, 130 | 131 | loadAndCreatePlayer: task(function * () { 132 | try { 133 | yield this.loadYouTubeApi(); 134 | let player = yield this.createPlayer(); 135 | 136 | this.setProperties({ 137 | player, 138 | playerState: 'ready' 139 | }); 140 | 141 | this.playerCreated(player); 142 | 143 | this.loadVideo(); 144 | } catch(err) { 145 | if (this.get('showDebug')) { 146 | debug(err); 147 | } 148 | 149 | throw err 150 | } 151 | }).drop(), 152 | 153 | // A promise that is resolved when window.onYouTubeIframeAPIReady is called. 154 | // The promise is resolved with a reference to window.YT object. 155 | loadYouTubeApi() { 156 | return new RSVP.Promise((resolve) => { 157 | let previous; 158 | previous = window.onYouTubeIframeAPIReady; 159 | 160 | // The API will call this function when page has finished downloading 161 | // the JavaScript for the player API. 162 | window.onYouTubeIframeAPIReady = () => { 163 | if (previous) { 164 | previous(); 165 | } 166 | resolve(window.YT); 167 | }; 168 | 169 | if (window.YT && window.YT.loaded) { 170 | // If already loaded, make sure not to load the script again. 171 | resolve(window.YT); 172 | } else { 173 | let ytScript = document.createElement("script"); 174 | ytScript.src = "https://www.youtube.com/iframe_api"; 175 | document.head.appendChild(ytScript); 176 | } 177 | }); 178 | }, 179 | 180 | // A promise that is immediately resolved with a YouTube player object. 181 | createPlayer() { 182 | const playerVars = this.get('playerVars'); 183 | const width = this.get('width'); 184 | const height = this.get('height'); 185 | const container = this.element.querySelector('.EmberYoutube-player'); 186 | let player; 187 | return new RSVP.Promise((resolve, reject) => { 188 | if (!container) { 189 | reject(`Couldn't find the container element to create a YouTube player`); 190 | } 191 | player = new YT.Player(container, { 192 | width, 193 | height, 194 | playerVars, 195 | events: { 196 | onReady() { 197 | resolve(player); 198 | }, 199 | onStateChange: this.onPlayerStateChange.bind(this), 200 | onError: this.onPlayerError.bind(this) 201 | } 202 | }); 203 | }); 204 | }, 205 | 206 | // Gets called by the YouTube player. 207 | onPlayerStateChange(event) { 208 | // Set a readable state name 209 | let state = this.get('stateNames.' + event.data.toString()); 210 | this.set('playerState', state); 211 | if (this.get('showDebug')) { 212 | debug(state); 213 | } 214 | // send actions outside 215 | this[state](event); 216 | this.playerStateChanged(event); 217 | // send actions inside 218 | this.send(state); 219 | }, 220 | 221 | // Gets called by the YouTube player. 222 | onPlayerError(event) { 223 | let errorCode = event.data; 224 | this.set('playerState', 'error'); 225 | // Send the event to the controller 226 | this.error(errorCode); 227 | if (this.get('showDebug')) { 228 | debug('error' + errorCode); 229 | } 230 | // switch(errorCode) { 231 | // case 2: 232 | // debug('Invalid parameter'); 233 | // break; 234 | // case 100: 235 | // debug('Not found/private'); 236 | // this.send('playNext'); 237 | // break; 238 | // case 101: 239 | // case 150: 240 | // debug('Embed not allowed'); 241 | // this.send('playNext'); 242 | // break; 243 | // default: 244 | // break; 245 | // } 246 | }, 247 | 248 | // Returns a boolean that indicates playback status by looking at the player state. 249 | isPlaying: computed('playerState', { 250 | get() { 251 | const player = this.get('player'); 252 | if (!player) { 253 | return false; 254 | } 255 | return player.getPlayerState() === 1; 256 | } 257 | }), 258 | 259 | // Load (and plays) a video every time ytid changes. 260 | ytidDidChange: observer('ytid', function () { 261 | const player = this.get('player'); 262 | const ytid = this.get('ytid'); 263 | 264 | if (!ytid) { 265 | return; 266 | } 267 | 268 | if (!player) { 269 | this.get('loadAndCreatePlayer').perform(); 270 | return; 271 | } 272 | this.loadVideo(); 273 | }), 274 | 275 | loadVideo() { 276 | const player = this.get('player'); 277 | const ytid = this.get('ytid'); 278 | 279 | // Set parameters for the video to be played. 280 | let options = getProperties(this, ['startSeconds', 'endSeconds', 'suggestedQuality']); 281 | options.videoId = ytid; 282 | // Either load or cue depending on `autoplay`. 283 | if (this.playerVars.autoplay) { 284 | player.loadVideoById(options); 285 | } else { 286 | player.cueVideoById(options); 287 | } 288 | }, 289 | 290 | updateTime() { 291 | const player = this.get('player'); 292 | if (player && player.getDuration && player.getCurrentTime) { 293 | this.set('currentTime', player.getCurrentTime()); 294 | this.set('duration', player.getDuration()); 295 | } 296 | }, 297 | 298 | startTimer() { 299 | // stop any previously started timer we forgot to clear 300 | this.stopTimer(); 301 | // set initial time by getting the computed properties 302 | this.updateTime(); 303 | // and also once every second so the progressbar is up to date 304 | let timer = window.setInterval(() => { 305 | this.updateTime(); 306 | }, 1000); 307 | // save the timer so we can stop it later 308 | this.set('timer', timer); 309 | }, 310 | 311 | stopTimer() { 312 | window.clearInterval(this.get('timer')); 313 | }, 314 | 315 | // A wrapper around the YouTube method to get current time. 316 | currentTime: computed({ 317 | get() { 318 | let player = this.get('player'); 319 | let value = player ? player.getCurrentTime() : 0; 320 | return value; 321 | }, 322 | set(key, value) { 323 | return value; 324 | } 325 | }), 326 | 327 | // A wrapper around the YouTube method to get the duration. 328 | duration: computed({ 329 | get() { 330 | let player = this.get('player'); 331 | let value = player ? player.getDuration() : 0; 332 | return value; 333 | }, 334 | set(key, value) { 335 | return value; 336 | } 337 | }), 338 | 339 | // A wrapper around the YouTube method to get and set volume. 340 | volume: computed({ 341 | get() { 342 | let player = this.get('player'); 343 | let value = player ? player.getVolume() : 0; 344 | return value; 345 | }, 346 | set(name, vol) { 347 | let player = this.get('player'); 348 | // Clamp between 0 and 100 349 | if (vol > 100) { 350 | vol = 100; 351 | } else if (vol < 0) { 352 | vol = 0; 353 | } 354 | if (player) { 355 | player.setVolume(vol); 356 | } 357 | return vol; 358 | } 359 | }), 360 | 361 | // OK, this is stupid but couldn't access the "event" inside 362 | // an ember action so here's a manual click handler instead. 363 | addProgressBarClickHandler() { 364 | this.element.addEventListener( 365 | "click", 366 | this.progressBarClick.bind(this), 367 | false 368 | ); 369 | }, 370 | progressBarClick(event) { 371 | let self = this; 372 | let element = event.srcElement; 373 | if (element.tagName.toLowerCase() !== "progress") return; 374 | // get the x position of the click inside our progress el 375 | let x = event.pageX - element.getBoundingClientRect().x; 376 | // convert it to a value relative to the duration (max) 377 | let clickedValue = (x * element.max) / element.offsetWidth; 378 | // 250 = 0.25 seconds into player 379 | self.set("currentTime", clickedValue); 380 | self.send("seekTo", clickedValue); 381 | }, 382 | removeProgressBarClickHandler() { 383 | this.element.removeEventListener( 384 | "click", 385 | this.progressBarClick.bind(this), 386 | false 387 | ); 388 | }, 389 | 390 | actions: { 391 | play() { 392 | if (this.get('player')) { 393 | this.get('player').playVideo(); 394 | } 395 | }, 396 | pause() { 397 | if (this.get('player')) { 398 | this.get('player').pauseVideo(); 399 | } 400 | }, 401 | togglePlay() { 402 | if (this.get('player') && this.get('isPlaying')) { 403 | this.send('pause'); 404 | } else { 405 | this.send('play'); 406 | } 407 | }, 408 | mute() { 409 | if (this.get('player')) { 410 | this.get('player').mute(); 411 | this.set('isMuted', true); 412 | } 413 | }, 414 | unMute() { 415 | if (this.get('player')) { 416 | this.get('player').unMute(); 417 | this.set('isMuted', false); 418 | } 419 | }, 420 | toggleVolume() { 421 | if (this.get('player').isMuted()) { 422 | this.send('unMute'); 423 | } else { 424 | this.send('mute'); 425 | } 426 | }, 427 | seekTo(seconds) { 428 | if (this.get('player')) { 429 | this.get('player').seekTo(seconds); 430 | } 431 | }, 432 | // YouTube events. 433 | ready() {}, 434 | ended() {}, 435 | playing() { 436 | this.startTimer(); 437 | }, 438 | paused() { 439 | this.stopTimer(); 440 | }, 441 | buffering() {}, 442 | queued() {} 443 | } 444 | }); 445 | --------------------------------------------------------------------------------