├── .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 | 
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:"