├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .npmignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── angular-video-bg-spec.js ├── angular-video-bg.js ├── angular-video-bg.min.js ├── bower.json ├── index.html ├── package.json └── screenshot.png /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/* 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": "nofunc", 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "browser": true, 13 | "smarttabs": true, 14 | "globals": { 15 | "jQuery": true, 16 | "angular": true, 17 | "console": true, 18 | "$": true, 19 | "_": true, 20 | "moment": true, 21 | "describe": true, 22 | "beforeEach": true, 23 | "module": true, 24 | "inject": true, 25 | "it": true, 26 | "expect": true, 27 | "xdescribe": true, 28 | "xit": true, 29 | "spyOn": true, 30 | "YT": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | temp/ 2 | dist/ 3 | node_modules/ 4 | _SpecRunner.html 5 | .DS_Store 6 | test-results.xml 7 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | 4 | var pkg = require('./package.json'); 5 | 6 | module.exports = function (grunt) { 7 | 8 | // load all grunt tasks 9 | require('load-grunt-tasks')(grunt); 10 | 11 | // Project configuration. 12 | grunt.initConfig({ 13 | connect: { 14 | main: { 15 | options: { 16 | port: 9001 17 | } 18 | } 19 | }, 20 | watch: { 21 | main: { 22 | options: { 23 | livereload: true, 24 | livereloadOnError: false, 25 | spawn: false 26 | }, 27 | files: ['./*.js','./*.html'], 28 | tasks: [] //all the tasks are run dynamically during the watch event handler 29 | } 30 | }, 31 | jshint: { 32 | main: { 33 | options: { 34 | jshintrc: '.jshintrc' 35 | }, 36 | src: ['angular-video-bg.js','angular-video-bg-spec.js'] 37 | } 38 | }, 39 | clean: { 40 | src:['temp'] 41 | }, 42 | strip : { 43 | main : { 44 | src: 'angular-video-bg.js', 45 | dest: 'temp/angular-video-bg.js' 46 | } 47 | }, 48 | uglify: { 49 | main: { 50 | src: 'temp/angular-video-bg.js', 51 | dest:'angular-video-bg.min.js' 52 | } 53 | }, 54 | karma: { 55 | options: { 56 | frameworks: ['jasmine'], 57 | files: [ //this files data is also updated in the watch handler, if updated change there too 58 | 'bower_components/angular/angular.js', 59 | 'bower_components/angular-mocks/angular-mocks.js', 60 | 'angular-video-bg.js', 61 | 'angular-video-bg-spec.js' 62 | ], 63 | logLevel:'ERROR', 64 | reporters:['mocha'], 65 | autoWatch: false, //watching is handled by grunt-contrib-watch 66 | singleRun: true 67 | }, 68 | all_tests: { 69 | browsers: ['PhantomJS','Chrome','Firefox'] 70 | }, 71 | during_watch: { 72 | browsers: ['PhantomJS'] 73 | } 74 | } 75 | }); 76 | 77 | grunt.registerTask('build',['jshint','clean','strip','uglify','clean']); 78 | grunt.registerTask('serve', ['jshint','connect', 'watch']); 79 | grunt.registerTask('test',['karma:all_tests']); 80 | 81 | grunt.event.on('watch', function(action, filepath) { 82 | //https://github.com/gruntjs/grunt-contrib-watch/issues/156 83 | 84 | var tasksToRun = []; 85 | 86 | if (filepath.lastIndexOf('.js') !== -1 && filepath.lastIndexOf('.js') === filepath.length - 3) { 87 | 88 | //lint the changed js file 89 | grunt.config('jshint.main.src', filepath); 90 | tasksToRun.push('jshint'); 91 | 92 | //find the appropriate unit test for the changed file 93 | var spec = filepath; 94 | if (filepath.lastIndexOf('-spec.js') === -1 || filepath.lastIndexOf('-spec.js') !== filepath.length - 8) { 95 | spec = filepath.substring(0,filepath.length - 3) + '-spec.js'; 96 | } 97 | 98 | //if the spec exists then lets run it 99 | if (grunt.file.exists(spec)) { 100 | tasksToRun.push('karma:during_watch'); 101 | } 102 | } 103 | 104 | grunt.config('watch.main.tasks',tasksToRun); 105 | 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Joel Kanzelmeyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-video-bg 2 | 3 | _NOTE: This project is no longer being maintained. I have been working primarily in React lately, and no longer have time to keep up with all of the Angular updates._ 4 | 5 | angular-video-bg is an [Angular.js](http://angularjs.org/) YouTube video background player directive. It stresses simplicity and performance. 6 | 7 | ![angular-video-bg Screenshot](https://raw.github.com/kanzelm3/angular-video-bg/master/screenshot.png) 8 | 9 | ## Demo 10 | 11 | Play with the [Plunker example](http://plnkr.co/edit/PR2oFbCeDoN3PCwAHMdg?p=preview) 12 | 13 | You can also see a demo of the directive here: [Angular YouTube Video Background](http://kanzelm3.github.io/angular-video-bg/) 14 | 15 | ## Download 16 | 17 | * [Latest Version](https://github.com/kanzelm3/angular-video-bg/zipball/master) 18 | 19 | You can also install the package using [Bower](http://bower.io). 20 | 21 | ```sh 22 | bower install angular-video-bg 23 | ``` 24 | 25 | Or add it to your bower.json file: 26 | 27 | ```javascript 28 | dependencies: { 29 | "angular-video-bg": "~0.3" 30 | } 31 | ``` 32 | 33 | *No dependencies (besides Angular) are required!* 34 | 35 | ## The Basics 36 | 37 | To use the library, include the JS file on your index page, then include the module in your app: 38 | 39 | ```javascript 40 | app = angular.module('myApp', ['angularVideoBg']) 41 | ``` 42 | 43 | The directive itself is simply called *video-bg*. The only required attribute is either videoId (which should be a YouTube 44 | video ID) or playlist (which should be an array of video objects, see example in advanced usage section below). 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | ## Inline Options 51 | 52 | There are a number of options that be configured inline with attributes: 53 | 54 | | Option | Default | Description | 55 | | -------------------- | ------------------- | ------------------------------------------------------------------------------------------- | 56 | | ratio | 16/9 | Aspect ratio for the supplied videoId. | 57 | | loop | true | If set to false, video will not automatically replay when it reaches the end. | 58 | | mute | true | If set to false, the video's sound will play. | 59 | | mobile-image | YT video thumb | Background image to display if user is on phone or tablet (videos cannot autoplay on mobile devices), default is YouTube video thumbnail. | 60 | | start | null | Video start time in seconds. If set, video will play from that point instead of beginning. | 61 | | end | null | Video end time in seconds. If set, video will play until that point and stop (or loop). | 62 | | content-z-index | 99 | If set, will replace the z-index of content within the directive. | 63 | | allow-click-events | false | If set to true, users will be able to click video to pause/play it. | 64 | | player-callback | null | If provided, player callback method will be called with the YouTube player object as the first and only argument. | 65 | 66 | **Example:** 67 | 68 | ```html 69 | 70 | ``` 71 | 72 | ## Advanced Usage 73 | 74 | The documentation above is sufficient for most use-cases; however, there are other options below for those that need more 75 | advanced integration. 76 | 77 | ### Playlist Capability 78 | 79 | If instead of playing a single video, you need to play several videos in a playlist, you should use the playlist attribute 80 | instead of the videoId attribute. The playlist attribute accepts an array of video objects. Each video object must have a 81 | 'videoId' property at minimum. Other valid properties that it can have are 'start', 'end', 'mute', and 'mobileImage'. These 82 | all do the same thing as the corresponding options on the directive, however, instead of applying to every video they only 83 | apply to the current video. Example below of using the playlist attribute: 84 | 85 | ```js 86 | angular.module('myApp').controller('VideoCtrl', function($scope) { 87 | 88 | // Array of videos to use as playlist 89 | $scope.videos = [{ 90 | videoId: 'some_video', 91 | mute: false 92 | },{ 93 | videoId: 'some_other_video', 94 | start: 10, 95 | end: 50 96 | }]; 97 | 98 | }); 99 | ``` 100 | 101 | ```html 102 | 103 | ``` 104 | 105 | If you dynamically change this videos array (e.g. add a new video to the list), the new playlist will be loaded and 106 | played accordingly. 107 | 108 | ### YouTube Player API 109 | 110 | If you need more control over the video (for example, if you need to play/pause the video on a button click), provide a 111 | method with "player" as the only argument to the player-callback attribute. 112 | 113 | ```html 114 | 115 | ``` 116 | 117 | ```javascript 118 | angular.module('myApp').controller(['$scope', function($scope) { 119 | $scope.callback = function(player) { 120 | $scope.pauseVideo = function() { 121 | player.pauseVideo(); 122 | }; 123 | $scope.playVideo = function() { 124 | player.playVideo(); 125 | }; 126 | }; 127 | }); 128 | ``` 129 | 130 | The player object gives you complete access to all of the methods and properties on the player in the 131 | [YouTube IFrame API](https://developers.google.com/youtube/iframe_api_reference#Playback_controls). 132 | 133 | ## Browser Support 134 | 135 | Tested and working in Chrome, Firefox, Safari, Opera and IE 9+. 136 | 137 | ## Contributing 138 | 139 | Contributions are welcome. Please be sure to document your changes. 140 | 141 | 1. Fork it 142 | 2. Create your feature branch (`git checkout -b my-new-feature`) 143 | 3. Commit your changes (`git commit -am 'Add some feature'`) 144 | 4. Push to the branch (`git push origin my-new-feature`) 145 | 5. Create new Pull Request 146 | 147 | To get the project running, you'll need [NPM](https://npmjs.org/) and [Bower](http://bower.io/). Run `npm install` and `bower install` to install all dependencies. Then run `grunt serve` in the project directory to watch and compile changes. 148 | 149 | If you create new unit test, you can run `grunt test` to execute all of the unit tests. Try to write tests if you contribute. 150 | 151 | ## Potential Features down the road 152 | 153 | * Add support for HTML5, Vimeo videos instead of just YouTube videos. 154 | -------------------------------------------------------------------------------- /angular-video-bg-spec.js: -------------------------------------------------------------------------------- 1 | describe('video-bg directive', function() { 2 | 3 | beforeEach(module('angularVideoBg')); 4 | 5 | var scope, compile, timeout, videoBg, videoBgScope, $player, $content; 6 | 7 | beforeEach(inject(function($rootScope, $compile, $timeout) { 8 | scope = $rootScope.$new(); 9 | compile = $compile; 10 | timeout = $timeout; 11 | 12 | videoBg = '

test

'; 13 | scope.videoId = 'M7lc1UVf-VE'; 14 | videoBg = compile(videoBg)(scope); 15 | scope.$digest(); 16 | 17 | $player = videoBg.children().eq(0); 18 | $content = videoBg.children().eq(1); 19 | videoBgScope = videoBg.isolateScope(); 20 | })); 21 | 22 | it('should create a video player element', function() { 23 | expect($player.attr('id')).toBe('player-1'); 24 | }); 25 | 26 | it('should have correct video id', function() { 27 | expect(videoBgScope.videoId).toBe('M7lc1UVf-VE'); 28 | }); 29 | 30 | it('should create a content element', function() { 31 | expect($content).toBeDefined(); 32 | }); 33 | 34 | describe('content element', function() { 35 | 36 | it('should content the transcluded content', function() { 37 | expect($content.find('p').text()).toBe('test'); 38 | }); 39 | 40 | }); 41 | 42 | describe('aspect ratio', function() { 43 | 44 | it('should be 16/9 by default', function() { 45 | expect(videoBgScope.ratio).toBe(16/9); 46 | }); 47 | 48 | it('should be changed if ratio attribute is added', function() { 49 | scope.ratio = 4/3; 50 | scope.$digest(); 51 | expect(videoBgScope.ratio).toBe(4/3); 52 | }); 53 | 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /angular-video-bg.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc overview 6 | * @name angularVideoBg 7 | * @description This module contains a directive that allows you easily make a YouTube video play as the background of 8 | * any container on your site. 9 | */ 10 | 11 | angular.module('angularVideoBg', []); 12 | 13 | /** 14 | * @ngdoc directive 15 | * @name angularVideoBg.directive:videoBg 16 | * @description This directive makes it super simple to turn the background of any element on your site into a YouTube 17 | * video. All you need is the video id! You can place content within the directive and it will be transcluded over top 18 | * of the video background. 19 | * @element 20 | */ 21 | angular.module('angularVideoBg').directive('videoBg', videoBg); 22 | 23 | // this obviates using ngAnnotate in the build task 24 | videoBg.$inject = ['$window', '$q', '$timeout']; 25 | 26 | function videoBg($window, $q, $timeout) { 27 | return { 28 | restrict: 'EA', 29 | replace: true, 30 | scope: { 31 | videoId: '=?', 32 | playlist: '=?', 33 | ratio: '=?', 34 | loop: '=?', 35 | mute: '=?', 36 | start: '=?', 37 | end: '=?', 38 | contentZIndex: '=?', 39 | allowClickEvents: '=?', 40 | mobileImage: '=?', 41 | playerCallback: '&?' 42 | }, 43 | transclude: true, 44 | template: '
', 45 | link: function(scope, element) { 46 | 47 | var computedStyles, 48 | ytScript = document.querySelector('script[src="//www.youtube.com/iframe_api"]'), 49 | $player = element.children().eq(0), 50 | playerId, 51 | player, 52 | parentDimensions, 53 | playerDimensions, 54 | playerCallback = scope.playerCallback, 55 | backgroundImage = scope.mobileImage || '//img.youtube.com/vi/' + scope.videoId + '/maxresdefault.jpg', 56 | videoArr, 57 | videoTimeout; 58 | 59 | playerId = 'player' + Array.prototype.slice.call(document.querySelectorAll('div[video-id]')).indexOf(element[0]); 60 | $player.attr('id', playerId); 61 | 62 | scope.ratio = scope.ratio || 16/9; 63 | scope.loop = scope.loop === undefined ? true : scope.loop; 64 | scope.mute = scope.mute === undefined ? true : scope.mute; 65 | 66 | if (!scope.videoId && !scope.playlist) { 67 | throw new Error('Either video-id or playlist must be defined.'); 68 | } 69 | if (scope.videoId && scope.playlist) { 70 | throw new Error('Both video-id and playlist cannot be defined, please choose one or the other.'); 71 | } 72 | if (scope.playlist) { 73 | videoArr = scope.playlist.map(function(videoObj) { 74 | return videoObj.videoId; 75 | }); 76 | } 77 | 78 | 79 | // Utility methods 80 | 81 | function debounce(func, wait) { 82 | var timeout; 83 | return function() { 84 | var context = this, args = arguments; 85 | var later = function() { 86 | timeout = null; 87 | func.apply(context, args); 88 | }; 89 | clearTimeout(timeout); 90 | timeout = setTimeout(later, wait); 91 | }; 92 | } 93 | 94 | /** 95 | * detect IE 96 | * returns version of IE or false, if browser is not Internet Explorer 97 | */ 98 | function detectIE() { 99 | var ua = window.navigator.userAgent, 100 | msie = ua.indexOf('MSIE '), 101 | trident = ua.indexOf('Trident/'), 102 | edge = ua.indexOf('Edge/'); 103 | 104 | if (msie > 0) { 105 | // IE 10 or older => return version number 106 | return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); 107 | } 108 | 109 | if (trident > 0) { 110 | // IE 11 => return version number 111 | var rv = ua.indexOf('rv:'); 112 | return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); 113 | } 114 | 115 | if (edge > 0) { 116 | // IE 12 => return version number 117 | return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); 118 | } 119 | 120 | // other browser 121 | return false; 122 | } 123 | 124 | /** 125 | * @ngdoc method 126 | * @name getPropertyAllSides 127 | * @methodOf angularVideoBg.directive:videoBg 128 | * @description This method takes a property such as margin and returns the computed styles for all four 129 | * sides of the parent container. 130 | * @param {string} property - the css property to get 131 | * @param {Function} func - the function to call on computedStyles 132 | * @returns {object} - object that contains all four property sides (top, right, bottom, top) 133 | * @example 134 | * getPropertyAllSides('margin', computedStyles.getPropertyValue); 135 | * // returns { margin-top: 10, margin-right: 10, margin-bottom: 10, margin-left: 10 } 136 | */ 137 | function getPropertyAllSides(property, func) { 138 | var sides = ['top', 'right', 'bottom', 'left'], 139 | getProperty = function(obj, side) { 140 | obj[side] = parseInt(func.call(computedStyles, property + '-' + side), 10); 141 | return obj; 142 | }; 143 | return sides.reduce(getProperty, {}); 144 | } 145 | 146 | /** 147 | * @ngdoc method 148 | * @name calculateParentDimensions 149 | * @methodOf angularVideoBg.directive:videoBg 150 | * @description This method takes the dimensions (width and height) of the parent, as well as the "spacers" 151 | * (simply all of the margin, padding and border values) and adds the margin, padding and border values to 152 | * the dimensions in order to get back the outer dimensions of the parent. 153 | * @param {object} dimensions - width and height of parent container 154 | * @param {object} spacers - margin, padding and border values of parent container 155 | * @returns {{width: number, height: number}} 156 | * @example 157 | * 158 | * var dimensions = { 159 | * width: 1000, 160 | * height: 400 161 | * }; 162 | * 163 | * var spacers = { 164 | * margin: { 165 | * top: 10, 166 | * right: 10, 167 | * bottom: 10, 168 | * left: 10 169 | * }, 170 | * padding: { 171 | * top: 0, 172 | * right: 10, 173 | * bottom: 0, 174 | * left: 10 175 | * }, 176 | * border: { 177 | * top: 0, 178 | * right: 0, 179 | * bottom: 0, 180 | * left: 0 181 | * } 182 | * }; 183 | * 184 | * calculateParentDimensions(dimensions, spacers); 185 | * // returns { width: 1040, height: 420 } 186 | * 187 | */ 188 | function calculateParentDimensions(dimensions, spacers) { 189 | function calculateSpacerValues() { 190 | var args = Array.prototype.slice.call(arguments), 191 | spacer, 192 | sum = 0, 193 | sumValues = function(_sum, arg) { 194 | return spacer[arg] ? _sum + spacer[arg] : _sum; 195 | }; 196 | for (var key in spacers) { 197 | if (spacers.hasOwnProperty(key)) { 198 | spacer = spacers[key]; 199 | sum += args.reduce(sumValues, 0); 200 | } 201 | } 202 | return sum; 203 | } 204 | return { 205 | width: dimensions.width + calculateSpacerValues('left', 'right'), 206 | height: (detectIE() && detectIE() < 12) ? dimensions.height : dimensions.height + calculateSpacerValues('top', 'bottom') 207 | }; 208 | } 209 | 210 | function styleContentElements() { 211 | var $content = element.children().eq(1), 212 | hasContent = !!$content.children().length, 213 | parentChildren = Array.prototype.slice.call(element.parent().children()); 214 | element.parent().css({ 215 | position: 'relative', 216 | overflow: 'hidden' 217 | }); 218 | if (!hasContent) { 219 | element.css({ 220 | position: 'absolute', 221 | left: '0', 222 | top: '0' 223 | }); 224 | var i = parentChildren.indexOf(element[0]); 225 | if (i > -1) { 226 | parentChildren.splice(i, 1); 227 | } 228 | $content = angular.element(parentChildren); 229 | } 230 | $content.css({ 231 | position: 'relative', 232 | zIndex: scope.contentZIndex || 99 233 | }); 234 | } 235 | 236 | /** 237 | * @ngdoc method 238 | * @name getParentDimensions 239 | * @methodOf angularVideoBg.directive:videoBg 240 | * @description This method utilizes the getPropertyAllSides and calculateParentDimensions in order to get 241 | * the parent container dimensions and return them. 242 | * @returns {{width: number, height: number}} 243 | */ 244 | function getParentDimensions() { 245 | computedStyles = $window.getComputedStyle(element.parent()[0]); 246 | var dimensionProperties = ['width', 'height'], 247 | spacerProperties = ['border', 'margin']; 248 | if (detectIE() && detectIE() < 12) { 249 | spacerProperties.push('padding'); 250 | } 251 | dimensionProperties = dimensionProperties.reduce(function(obj, property) { 252 | obj[property] = parseInt(computedStyles.getPropertyValue(property), 10); 253 | return obj; 254 | }, {}); 255 | spacerProperties = spacerProperties.reduce(function(obj, property) { 256 | obj[property] = getPropertyAllSides(property, computedStyles.getPropertyValue); 257 | return obj; 258 | }, {}); 259 | return calculateParentDimensions(dimensionProperties, spacerProperties); 260 | } 261 | 262 | /** 263 | * @ngdoc method 264 | * @name getPlayerDimensions 265 | * @methodOf angularVideoBg.directive:videoBg 266 | * @description This method uses the aspect ratio of the video and the height/width of the parent container 267 | * in order to calculate the width and height of the video player. 268 | * @returns {{width: number, height: number}} 269 | */ 270 | function getPlayerDimensions() { 271 | var aspectHeight = parseInt(parentDimensions.width / scope.ratio, 10), 272 | aspectWidth = parseInt(parentDimensions.height * scope.ratio, 10), 273 | useAspectHeight = parentDimensions.height < aspectHeight; 274 | return { 275 | width: useAspectHeight ? parentDimensions.width : aspectWidth, 276 | height: useAspectHeight ? aspectHeight : parentDimensions.height 277 | }; 278 | } 279 | 280 | /** 281 | * This method simply executes getParentDimensions and getPlayerDimensions when necessary. 282 | */ 283 | function updateDimensions() { 284 | styleContentElements(); 285 | parentDimensions = getParentDimensions(); 286 | playerDimensions = getPlayerDimensions(); 287 | } 288 | 289 | /** 290 | * This method simply resizes and repositions the player based on the dimensions of the parent and video 291 | * player, it is called when necessary. 292 | */ 293 | function resizeAndPositionPlayer() { 294 | var options = { 295 | zIndex: 1, 296 | position: 'absolute', 297 | width: playerDimensions.width + 'px', 298 | height: playerDimensions.height + 'px', 299 | left: parseInt((parentDimensions.width - playerDimensions.width)/2, 10) + 'px', 300 | top: parseInt((parentDimensions.height - playerDimensions.height)/2, 10) + 'px' 301 | }; 302 | if (!scope.allowClickEvents) { 303 | options.pointerEvents = 'none'; 304 | } 305 | $player.css(options); 306 | } 307 | 308 | /** 309 | * This method simply seeks the video to either the beginning or to the start position (if set). 310 | */ 311 | function seekToStart(video) { 312 | video = video || scope; 313 | player.seekTo(video.start || 0); 314 | } 315 | 316 | /** 317 | * This method handles looping the video better than the native YT embed API player var "loop", especially 318 | * when start and end positions are set. 319 | */ 320 | function loopVideo(video) { 321 | var duration, msDuration; 322 | video = video || scope; 323 | if (video.end) { 324 | duration = video.end - (video.start || 0); 325 | } else if (scope.start) { 326 | duration = player.getDuration() - video.start; 327 | } else { 328 | duration = player.getDuration(); 329 | } 330 | msDuration = duration * 1000; 331 | console.log('duration', msDuration); 332 | videoTimeout = setTimeout(function() { 333 | if (scope.playlist) { 334 | player.nextVideo(); 335 | } else { 336 | seekToStart(video); 337 | } 338 | }, msDuration); 339 | } 340 | 341 | /** 342 | * This method handles looping the video better than the native YT embed API player var "loop", especially 343 | * when start and end positions are set. 344 | */ 345 | function playlistVideoChange() { 346 | var videoObj = scope.playlist[player.getPlaylistIndex()]; 347 | loopVideo(videoObj); 348 | } 349 | 350 | /** 351 | * This is the method called when the "player" object is ready and can be interfaced with. 352 | */ 353 | function playerReady() { 354 | if (playerCallback) { 355 | $timeout(function() { 356 | playerCallback({ player: player }); 357 | }); 358 | } 359 | if (scope.playlist) { 360 | player.loadPlaylist(videoArr); 361 | if (scope.loop) { 362 | player.setLoop(true); 363 | } 364 | } 365 | if (scope.mute && !player.isMuted()) { 366 | player.mute(); 367 | } else if (player.isMuted()) { 368 | player.unMute(); 369 | } 370 | seekToStart(); 371 | scope.$on('$destroy', function() { 372 | if (videoTimeout) { 373 | clearTimeout(videoTimeout); 374 | } 375 | angular.element($window).off('resize', windowResized); 376 | player.destroy(); 377 | }); 378 | } 379 | 380 | /** 381 | * This is the method called when the "player" object has changed state. It is used here to toggle the video's 382 | * display css to block only when the video starts playing, and kick off the video loop (if enabled). 383 | */ 384 | function playerStateChange(evt) { 385 | if (evt.data === YT.PlayerState.PLAYING) { 386 | $player.css('display', 'block'); 387 | if (!scope.playlist && scope.loop) { 388 | loopVideo(); 389 | } 390 | if (scope.playlist && scope.loop) { 391 | playlistVideoChange(); 392 | } 393 | } 394 | if (evt.data === YT.PlayerState.UNSTARTED && scope.playlist) { 395 | var videoObj = scope.playlist[player.getPlaylistIndex()], 396 | videoMute = videoObj.mute === undefined ? scope.mute : videoObj.mute; 397 | backgroundImage = videoObj.mobileImage || scope.mobileImage || '//img.youtube.com/vi/' + videoObj.videoId + '/maxresdefault.jpg'; 398 | setBackgroundImage(backgroundImage); 399 | $player.css('display', 'none'); 400 | seekToStart(videoObj); 401 | if (videoMute || (videoMute && scope.mute)) { 402 | console.log('mute'); 403 | if (!player.isMuted()) { 404 | player.mute(); 405 | } 406 | } else if (!videoMute || !scope.mute) { 407 | console.log('unmute'); 408 | if (player.isMuted()) { 409 | player.unMute(); 410 | } 411 | } 412 | } 413 | } 414 | 415 | /** 416 | * This method initializes the video player and updates the dimensions and positions for the first time. 417 | */ 418 | function initVideoPlayer() { 419 | updateDimensions(); 420 | var playerOptions = { 421 | autoplay: 1, 422 | controls: 0, 423 | iv_load_policy: 3, 424 | cc_load_policy: 0, 425 | modestbranding: 1, 426 | playsinline: 1, 427 | rel: 0, 428 | showinfo: 0, 429 | playlist: scope.videoId 430 | }; 431 | player = new YT.Player(playerId, { 432 | width: playerDimensions.width, 433 | height: playerDimensions.height, 434 | videoId: scope.videoId, 435 | playerVars: playerOptions, 436 | events: { 437 | onReady: playerReady, 438 | onStateChange: playerStateChange 439 | } 440 | }); 441 | $player = element.children().eq(0); 442 | $player.css('display', 'none'); 443 | resizeAndPositionPlayer(); 444 | } 445 | 446 | function setBackgroundImage(img) { 447 | element.parent().css({ 448 | backgroundImage: 'url(' + img + ')', 449 | backgroundSize: 'cover', 450 | backgroundPosition: 'center center' 451 | }); 452 | } 453 | 454 | var windowResized = debounce(function() { 455 | updateDimensions(); 456 | resizeAndPositionPlayer(); 457 | }, 300); 458 | 459 | setBackgroundImage(backgroundImage); 460 | 461 | /** 462 | * if it's not mobile or tablet then initialize video 463 | */ 464 | if( !/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { 465 | var ytd; 466 | /** 467 | * Check to see if YouTube IFrame script is ready, if it is, resolve ytd defer, if not, wait for 468 | * onYouTubeIframeAPIReady to be called by the script to resolve it. 469 | */ 470 | if (!$window.youTubeIframeAPIReady) { 471 | ytd = $q.defer(); 472 | $window.youTubeIframeAPIReady = ytd.promise; 473 | $window.onYouTubeIframeAPIReady = function() { 474 | ytd.resolve(); 475 | }; 476 | } 477 | 478 | /** 479 | * If YouTube IFrame Script hasn't been loaded, load the library asynchronously 480 | */ 481 | if (!ytScript) { 482 | var tag = document.createElement('script'); 483 | tag.src = "//www.youtube.com/iframe_api"; 484 | var firstScriptTag = document.getElementsByTagName('script')[0]; 485 | firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 486 | } else if (ytd) { 487 | ytd.resolve(); 488 | } 489 | 490 | /** 491 | * When the YouTube IFrame API script is loaded, we initialize the video player. 492 | */ 493 | $window.youTubeIframeAPIReady.then(initVideoPlayer); 494 | 495 | /** 496 | * Anytime the window is resized, update the video player dimensions and position. (this is debounced for 497 | * performance reasons) 498 | */ 499 | angular.element($window).on('resize', windowResized); 500 | 501 | } 502 | 503 | scope.$watch('videoId', function(current, old) { 504 | if (current && old && current !== old) { 505 | clearTimeout(videoTimeout); 506 | backgroundImage = scope.mobileImage || '//img.youtube.com/vi/' + current + '/maxresdefault.jpg'; 507 | setBackgroundImage(backgroundImage); 508 | $player.css('display', 'none'); 509 | player.loadVideoById(current); 510 | } 511 | }); 512 | 513 | scope.$watchCollection('playlist', function(current, old) { 514 | if (current && old && current !== old) { 515 | clearTimeout(videoTimeout); 516 | videoArr = current.map(function(videoObj) { 517 | return videoObj.videoId; 518 | }); 519 | player.loadPlaylist(videoArr); 520 | if (scope.loop) { 521 | player.setLoop(true); 522 | } 523 | } 524 | }); 525 | 526 | } 527 | }; 528 | } 529 | 530 | })(); 531 | -------------------------------------------------------------------------------- /angular-video-bg.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function a(a,b,c){return{restrict:"EA",replace:!0,scope:{videoId:"=?",playlist:"=?",ratio:"=?",loop:"=?",mute:"=?",start:"=?",end:"=?",contentZIndex:"=?",allowClickEvents:"=?",mobileImage:"=?",playerCallback:"&?"},transclude:!0,template:"
",link:function(d,e){function f(a,b){var c;return function(){var d=this,e=arguments,f=function(){c=null,a.apply(d,e)};clearTimeout(c),c=setTimeout(f,b)}}function g(){var a=window.navigator.userAgent,b=a.indexOf("MSIE "),c=a.indexOf("Trident/"),d=a.indexOf("Edge/");if(b>0)return parseInt(a.substring(b+5,a.indexOf(".",b)),10);if(c>0){var e=a.indexOf("rv:");return parseInt(a.substring(e+3,a.indexOf(".",e)),10)}return d>0?parseInt(a.substring(d+5,a.indexOf(".",d)),10):!1}function h(a,b){var c=["top","right","bottom","left"],d=function(c,d){return c[d]=parseInt(b.call(v,a+"-"+d),10),c};return c.reduce(d,{})}function i(a,b){function c(){var a,c=Array.prototype.slice.call(arguments),d=0,e=function(b,c){return a[c]?b+a[c]:b};for(var f in b)b.hasOwnProperty(f)&&(a=b[f],d+=c.reduce(e,0));return d}return{width:a.width+c("left","right"),height:g()&&g()<12?a.height:a.height+c("top","bottom")}}function j(){var a=e.children().eq(1),b=!!a.children().length,c=Array.prototype.slice.call(e.parent().children());if(e.parent().css({position:"relative",overflow:"hidden"}),!b){e.css({position:"absolute",left:"0",top:"0"});var f=c.indexOf(e[0]);f>-1&&c.splice(f,1),a=angular.element(c)}a.css({position:"relative",zIndex:d.contentZIndex||99})}function k(){v=a.getComputedStyle(e.parent()[0]);var b=["width","height"],c=["border","margin"];return g()&&g()<12&&c.push("padding"),b=b.reduce(function(a,b){return a[b]=parseInt(v.getPropertyValue(b),10),a},{}),c=c.reduce(function(a,b){return a[b]=h(b,v.getPropertyValue),a},{}),i(b,c)}function l(){var a=parseInt(y.width/d.ratio,10),b=parseInt(y.height*d.ratio,10),c=y.height 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 58 | 59 |
60 | 61 |

Testing transclude!

62 | 63 | 64 | 65 | 66 |
67 |
68 | 69 |

Testing transclude 2!

70 |
71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularVideoBg", 3 | "version": "0.3.4", 4 | "devDependencies": { 5 | "grunt": "~0.4", 6 | "grunt-contrib-clean": "~0.5", 7 | "grunt-contrib-concat": "~0.3", 8 | "grunt-contrib-connect": "~0.6", 9 | "grunt-contrib-jshint": "~0.9", 10 | "grunt-contrib-uglify": "~0.2", 11 | "grunt-contrib-watch": "~0.6", 12 | "grunt-karma": "~0.8.3", 13 | "grunt-strip": "^0.2.1", 14 | "karma": "~0.12.6", 15 | "karma-chrome-launcher": "~0.1.3", 16 | "karma-firefox-launcher": "~0.1.3", 17 | "karma-jasmine": "~0.1.5", 18 | "karma-mocha-reporter": "~0.2.5", 19 | "karma-phantomjs-launcher": "~0.1.4", 20 | "load-grunt-tasks": "~0.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanzelm3/angular-video-bg/92a8e9307233d92fc7920368b131408cce725e74/screenshot.png --------------------------------------------------------------------------------