├── example ├── partials │ ├── nav-switcher.html │ ├── nav-scroller.html │ ├── nav-switcher-item.html │ ├── nav-scroller-item.html │ ├── 404.html │ ├── fin.html │ ├── intro.html │ ├── video.html │ └── video.overlay.html ├── assets │ ├── img │ │ ├── bg.jpg │ │ └── anigif.gif │ ├── captions │ │ └── bunny-en.vtt │ └── css │ │ └── ng-media.example.css ├── js │ ├── controllers │ │ ├── videoController.js │ │ └── 404.js │ ├── directives │ │ ├── addClass.js │ │ ├── switcher.js │ │ └── nav.js │ └── app.js └── index.html ├── .gitignore ├── lib ├── travis │ └── build.sh └── grunt │ └── index.js ├── css └── video.css ├── test ├── helpers │ └── setup.helper.js └── video.spec.js ├── bower.json ├── .travis.yml ├── package.json ├── karma.conf.js ├── README.md ├── Gruntfile.js └── src └── video.js /example/partials/nav-switcher.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /example/partials/nav-scroller.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /example/partials/nav-switcher-item.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/assets/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caitp/ng-media/HEAD/example/assets/img/bg.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /bower_components 3 | /node_modules 4 | /build 5 | /coverage 6 | *~ 7 | *.bak 8 | -------------------------------------------------------------------------------- /example/assets/img/anigif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caitp/ng-media/HEAD/example/assets/img/anigif.gif -------------------------------------------------------------------------------- /example/partials/nav-scroller-item.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /lib/travis/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | grunt bower jshint build 6 | karma start karma.conf.js --browsers SL_Firefox,SL_Chrome --single-run 7 | -------------------------------------------------------------------------------- /example/partials/404.html: -------------------------------------------------------------------------------- 1 |

404

2 | 3 |

The page you're looking for does not exist.

4 | 5 |

Redirecting in {{time}}

6 | -------------------------------------------------------------------------------- /example/js/controllers/videoController.js: -------------------------------------------------------------------------------- 1 | app 2 | .controller({ 3 | videoController: function($scope) { 4 | $scope.mode = "example"; 5 | }, 6 | videoOverlayController: function($scope) { 7 | $scope.mode = "example"; 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /example/js/directives/addClass.js: -------------------------------------------------------------------------------- 1 | app 2 | .directive('addClass', function($timeout) { 3 | return function(scope, element, attr) { 4 | $timeout(function() { 5 | element.addClass(attr.addClass); 6 | }, 666); // this is pretty arbitrary, I know. 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /example/js/controllers/404.js: -------------------------------------------------------------------------------- 1 | app.controller('404Controller', function($scope, $location, $timeout) { 2 | $scope.time = 10; 3 | $scope.home = '/'; 4 | $timeout(countdown, 1000); 5 | 6 | function countdown() { 7 | if ($scope.time-- === 0) { 8 | $location.path('/'); 9 | } else { 10 | $timeout(countdown, 1000); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /css/video.css: -------------------------------------------------------------------------------- 1 | .html5-video { 2 | position: relative; 3 | overflow: auto; 4 | display: inline-block; 5 | margin: 0; 6 | } 7 | 8 | .html5-video-overlay { 9 | margin: 0; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | z-index: 100; 14 | background: none; 15 | overflow: hidden; 16 | pointer-events:none; 17 | width: 100%; 18 | height: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /test/helpers/setup.helper.js: -------------------------------------------------------------------------------- 1 | angular.module('media.tests', []) 2 | .directive({ 3 | 'html5Video': html5Video 4 | }) 5 | .controller({ 6 | 'html5VideoController': html5VideoController 7 | }); 8 | 9 | beforeEach(function() { 10 | module('media.tests'); 11 | }); 12 | 13 | var linkCSS = function(path) { 14 | var link = document.createElement('link'); 15 | link.href = path; 16 | link.rel = "stylesheet"; 17 | link.type = "text/css"; 18 | document.head.appendChild(link); 19 | }; 20 | 21 | linkCSS("/base/css/video.css"); 22 | -------------------------------------------------------------------------------- /example/assets/captions/bunny-en.vtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | 3 | first 4 | 00:00:00.000 --> 00:00:04.000 5 | The Peach Open Movie Project presents 6 | 7 | second 8 | 00:00:06.500 --> 00:00:09.000 9 | One big rabbit 10 | 11 | 3 12 | 00:00:11.000 --> 00:00:13.000 13 | Three rodents 14 | 15 | 00:00:16.500 --> 00:00:19.000 16 | And one giant payback 17 | 18 | five 19 | 00:00:23.000 --> 00:00:25.000 20 | Get ready 21 | 22 | 00:00:27.000 --> 00:00:30.000 23 | Big Buck Bunny 24 | 25 | 00:00:30.000 --> 00:00:31.000 26 | Coming soon 27 | 28 | eight 29 | 00:00:31.000 --> 00:00:33.000 30 | www.bigbuckbunny.org 31 | Licensed as Creative Commons 3.0 attribution -------------------------------------------------------------------------------- /example/partials/fin.html: -------------------------------------------------------------------------------- 1 |

fin

2 | 3 |

This concludes the tour of ng-media, for the time being. More demonstrations should be 4 | available as more functionality is added to the module!

5 | 6 |

Though the list of features is not at all exhaustive at this time, it is hoped that this can be 7 | cleanly expanded on. Here are some of the things we'd like to support:

8 | 9 | 18 | 19 |

I hope you find this interesting and useful, and I look forward to hearing your comments.

-------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-media", 3 | "version": "0.1.0", 4 | "authors": [ 5 | "Caitlin Potter " 6 | ], 7 | "description": "AngularJS support for HTML5 media elements", 8 | "keywords": [ 9 | "AngularJS", 10 | "Front-end", 11 | "UI", 12 | "UX", 13 | "Framework", 14 | "Design", 15 | "Web Components", 16 | "Media", 17 | "Video", 18 | "Audio", 19 | "VP8", 20 | "H264" 21 | ], 22 | "license": "MIT", 23 | "homepage": "http://caitp.github.io/ng-ink", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ], 31 | "dependencies": { 32 | "angular": "1.2.13", 33 | "angular-animate": "1.2.13", 34 | "angular-mocks": "1.2.13", 35 | "angular-route": "1.2.13" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | 5 | env: 6 | global: 7 | - secure: "HJmJFfI3+phyTwd4qBMgSaNei9XMCuH81lrNHC3jmWISsZF47Cgn5R0Yuei83BiXkPX8GBoWZloujepGS49A5n3CO4h4XZm+JDzFyScuF66I/RMj4Fi0OZr0hc4AbHq+U8+xs7kA0dyEbvBzB5S65NGqMTNNfVYBGf3rEJrjNqg=" 8 | - secure: "iT05KE/GW6tM3lqFKlF38ATL+G37iqmcw5KyLCwcj6TMcFljWbeRypKiK0kF/B9h8O01vQMMaNnxOyfG/sX01vieJht/MWS/15l0o+ZBBfR0M4Sz08NqtJCHIrViaMoteeXalDjVtokLV+mvVMYVSv3ojYVmgPJRdSFNCMZ1piU=" 9 | - secure: "Snq8RYHWXcaxXCDIx3kX8h8RDb3pyDK6DAzr0Hn4oW5qVE4OAr1ZSBPUarfjH9CAw+nDjVRuWbnLBvdecbcnWd4c8MmSl84XxF6hTV8IshqZOTl99OT+9TSUkf0H1sDDFJwDsLrmOY0APbjrTG9Jyy40YmA/GZCwZ+f8MKa5gOo=" 10 | 11 | before_script: 12 | - npm install --silent -g grunt-cli karma-cli 13 | - npm install --dev --silent 14 | - npm install karma-sauce-launcher 15 | 16 | script: ./lib/travis/build.sh 17 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ng-media 5 | 6 | 7 | 8 | 9 | 10 | Fork me on GitHub 13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/js/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("media.example", ["ngAnimate", "ngRoute", "media"]); 2 | 3 | var tour = [ 4 | { url: '/', description: 'ng-media for AngularJS' }, 5 | { url: '/video', description: 'Directive: html5-video' }, 6 | { url: '/video/overlay', description: 'html5-video overlay' }, 7 | { url: '/fin', description: 'End of tour' } 8 | ]; 9 | 10 | app 11 | .config(function($routeProvider) { 12 | $routeProvider 13 | .when('', { 14 | templateUrl: 'partials/intro.html' 15 | }) 16 | .when('/', { 17 | templateUrl: 'partials/intro.html' 18 | }) 19 | .when('/video', { 20 | templateUrl: 'partials/video.html', 21 | controller: 'videoController' 22 | }) 23 | .when('/video/overlay', { 24 | templateUrl: 'partials/video.overlay.html', 25 | controller: 'videoOverlayController' 26 | }) 27 | .when('/fin', { 28 | templateUrl: 'partials/fin.html' 29 | }) 30 | .when('/404', { 31 | templateUrl: 'partials/404.html', 32 | controller: '404Controller' 33 | }) 34 | .otherwise({ redirectTo: '/404' }); 35 | }) 36 | .run(function($rootScope) { 37 | $rootScope.navoptions = tour; 38 | $rootScope.$on('$routeChangeError', function($event) { 39 | $location.path('/404'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-media", 3 | "version": "0.1.0", 4 | "description": "AngularJS support for HTML5 media elements", 5 | "scripts": { 6 | "test": "grunt test" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "http://github.com/caitp/ng-media.git" 11 | }, 12 | "keywords": [ 13 | "AngularJS", 14 | "Front-end", 15 | "UI", 16 | "UX", 17 | "Framework", 18 | "Design", 19 | "Web Components", 20 | "Media", 21 | "Video", 22 | "Audio", 23 | "VP8", 24 | "H264" 25 | ], 26 | "author": "Caitlin Potter", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/caitp/ng-media/issues" 30 | }, 31 | "homepage": "https://github.com/caitp/ng-media", 32 | "devDependencies": { 33 | "grunt": "~0.4.1", 34 | "grunt-contrib-jshint": "~0.11.0", 35 | "bower": "~1.4.0", 36 | "grunt-contrib-copy": "~0.8.0", 37 | "grunt-parallel": "~0.4.1", 38 | "grunt-contrib-connect": "~0.10.1", 39 | "grunt-contrib-watch": "~0.6.1", 40 | "grunt-contrib-concat": "~0.5.0", 41 | "grunt-gh-pages": "~0.10.0", 42 | "karma-html2js-preprocessor": "~0.1.0", 43 | "karma-jasmine": "~0.3.2", 44 | "requirejs": "~2.1.9", 45 | "karma-requirejs": "~0.2.0", 46 | "karma": "~0.12.3", 47 | "grunt-karma": "~0.11.0", 48 | "karma-coverage": "^0.4.2", 49 | "karma-coveralls": "^1.1.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/partials/intro.html: -------------------------------------------------------------------------------- 1 |

ng-media

2 | 3 |

The ng-media library's goal is to make life easier when working with media content in Angular apps 4 | on the web. The module is offered entirely under the MIT license, free to use by any individual or 5 | organization who finds a use for it. In the off-chance that you do NOT find a use for it, 6 | despite having a need for media in an AngularJS environment, please let me know what you feel 7 | it's lacking on the issue tracker.

8 | 9 |

This is perhaps a fairly ambitious project, as multimedia is finding a place on the web more and 10 | more frequently. This module attempts to simplify certain aspects of media integration, from within 11 | the AngularJS ecosystem.

12 | 13 |

Things we'd like to accomplish include support for all of the various HTML5 media APIs, including 14 | WebRTC, WebAudio, beyond just the HTML5 media elements. It's also desirable to add features to 15 | these media elements which are not strictly speaking present or defined in the spec.

16 | 17 |

Rather than bore you with a pointless manifesto, lets show off some basic functionality and get 18 | a look at things. There isn't much to see yet, but I promise this will be improved over time.

19 | 20 |

You can navigate this website using the mousewheel, using your keyboard up/down keys, or by 21 | simply clicking the links on the righthand side of the screen.

-------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '.', 4 | frameworks: ['jasmine'], 5 | autoWatch: true, 6 | logLevel: config.LOG_INFO, 7 | logColors: true, 8 | browsers: ['Firefox'], 9 | browserDisconnectTimeout: 5000, 10 | 11 | files: [ 12 | 'bower_components/angular/angular.js', 13 | 'bower_components/angular-mocks/angular-mocks.js', 14 | 'css/**/*.css', 15 | 'src/**/*.js', 16 | 'test/**/*.helper.js', 17 | 'test/**/*.spec.js' 18 | ], 19 | 20 | reporters: ['dots', 'coverage'], 21 | 22 | preprocessors: { 23 | 'src/**/*.js': ['coverage'] 24 | }, 25 | 26 | junitReporter: { 27 | outputFile: 'test_out/jqlite.xml', 28 | suite: 'jqLite' 29 | }, 30 | 31 | coverageReporter: { 32 | reporters: [{ 33 | type: 'lcov', 34 | dir: 'coverage/' 35 | }, { 36 | type: 'text' 37 | }] 38 | }, 39 | 40 | sauceLabs: { 41 | testName: 'ngMedia', 42 | startConnect: true, 43 | options: { 44 | 'selenium-version': '2.37.0' 45 | } 46 | }, 47 | 48 | 49 | customLaunchers: { 50 | // Sauce Labs browsers 51 | 'SL_Chrome': { 52 | base: 'SauceLabs', 53 | browserName: 'chrome' 54 | }, 55 | 'SL_Firefox': { 56 | base: 'SauceLabs', 57 | browserName: 'firefox', 58 | version: '26' 59 | }, 60 | 'SL_Safari': { 61 | base: 'SauceLabs', 62 | browserName: 'safari', 63 | platform: 'OS X 10.9', 64 | version: '7' 65 | } 66 | }, 67 | }); 68 | 69 | if (process.env.TRAVIS) { 70 | config.reporters.push('coveralls'); 71 | config.sauceLabs.build = 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; 72 | config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; 73 | 74 | // TODO(caitp): remove once SauceLabs supports websockets. 75 | // This speeds up the capturing a bit, as browsers don't even try to use websocket. 76 | config.transports = ['xhr-polling']; 77 | } 78 | 79 | function arrayRemove(array, item) { 80 | var index = array.indexOf(item); 81 | if (index >= 0) { 82 | array.splice(index, 1); 83 | } 84 | } 85 | if (process.argv.indexOf('--debug') >= 0) { 86 | arrayRemove(config.reporters, 'coverage'); 87 | for (var key in config.preprocessors) { 88 | arrayRemove(config.preprocessors[key], 'coverage'); 89 | } 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /example/js/directives/switcher.js: -------------------------------------------------------------------------------- 1 | app 2 | .directive('switcher', function($compile) { 3 | return { 4 | restrict: "A", 5 | require: ['switcher', 'ngModel'], 6 | templateUrl: 'partials/nav-switcher.html', 7 | replace: true, 8 | controller: function() { 9 | var items = [], current; 10 | this.$addItem = function(navItem) { 11 | items.push(navItem); 12 | if (navItem.$name() === this.$model.$viewValue) { 13 | this.$select(navItem); 14 | } 15 | }; 16 | this.$find = function(item) { 17 | for (var i = 0; i < items.length; ++i) { 18 | var $it = items[i]; 19 | if ($it.$name() === item) { 20 | return $it; 21 | } 22 | } 23 | }; 24 | this.$select = function(item) { 25 | if (typeof item === 'string') { 26 | item = this.$find(item); 27 | } 28 | if (!item) { 29 | return; 30 | } 31 | if (current) { 32 | current = current.$deselect(); 33 | } 34 | current = item.$select(); 35 | this.$model.$setViewValue(item.$name()); 36 | }; 37 | }, 38 | link: function($scope, $element, $attr, $controllers) { 39 | var $self = $controllers[0], $model = $controllers[1]; 40 | $self.$model = $model; 41 | var items = ($attr.switcher || '').split('|'); 42 | angular.forEach(items, function(item) { 43 | var template = ''; 44 | $compile(template)($scope, function(dom) { 45 | $element.append(dom); 46 | }); 47 | }); 48 | $model.$formatters.unshift(function(value) { 49 | $self.$select(value); 50 | return value; 51 | }); 52 | } 53 | }; 54 | }) 55 | .directive('switcherItem', function() { 56 | return { 57 | restrict: "A", 58 | require: ['switcherItem', '^switcher'], 59 | templateUrl: 'partials/nav-switcher-item.html', 60 | replace: true, 61 | scope: true, 62 | controller: function($scope, $element, $attrs) { 63 | var name = $scope.switcherItem = $attrs.switcherItem || ''; 64 | this.$name = function() { 65 | return name; 66 | }; 67 | this.$select = function() { 68 | $attrs.$addClass('active'); 69 | return this; 70 | }; 71 | this.$deselect = function() { 72 | $attrs.$removeClass('active'); 73 | } 74 | }, 75 | link: function($scope, $element, $attr, $controllers) { 76 | $controllers[1].$addItem($controllers[0]); 77 | $element.bind('click touch', function() { 78 | $scope.$apply(function() { 79 | $controllers[1].$select($controllers[0]); 80 | }); 81 | }); 82 | } 83 | }; 84 | }); 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##ng-media [![Build Status](https://travis-ci.org/caitp/ng-media.svg?branch=master)](https://travis-ci.org/caitp/ng-media) [![devDependency Status](https://david-dm.org/caitp/ng-media/dev-status.svg?theme=shields.io)](https://david-dm.org/caitp/ng-media#info=devDependencies) [![Coverage Status](http://img.shields.io/coveralls/caitp/ng-media/master.svg)](https://coveralls.io/r/caitp/ng-media) 2 | 3 | ###[AngularJS](http://angularjs.org/) support for HTML5 media elements 4 | 5 | ng-media provides a simple, declarative means for using HTML5 audio and video elements. 6 | 7 | A simple example: 8 | 9 | ```html 10 |
13 |

14 | HTML, including custom video controls, can be overlayed easily over the 15 | video frame. 16 |

17 |
18 | ``` 19 | 20 | More advanced features are also possible: 21 | 22 | ```js 23 | $scope.videoSources = [{ 24 | src: "media/farmville.webm", 25 | type: "video/webm", 26 | media: "screen" 27 | }, 'media/farmville.mp4']; 28 | 29 | $scope.videoTracks = { 30 | src: "captions/moocow.vtt", 31 | kind: "captions", 32 | type: "text/vtt" 33 | }; 34 | ``` 35 | 36 | ```html 37 | 46 |
49 | ``` 50 | 51 | **coming soon** 52 | 53 | - Register handlers for media events 54 | - Improved video controller API 55 | - Integrated support for playlists 56 | - Audio directive 57 | - Live demo 58 | - ngdocs page 59 | 60 | **hopefully some day** 61 | 62 | - DSP effects (one can dream!) 63 | - WebRTC support 64 | 65 | ###Contributing 66 | 67 | Pull requests, bug reports and suggestions are quite welcome. 68 | 69 | Code submissions should follow the [Google JavaScript Style Guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml), and each and every new feature or bug fix should incorporate one or more meaningful tests to assist in preventing future regressions. 70 | 71 | ###License 72 | 73 | The MIT License (MIT) 74 | 75 | Copyright (c) 2013 Caitlin Potter & Contributors 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining a copy 78 | of this software and associated documentation files (the "Software"), to deal 79 | in the Software without restriction, including without limitation the rights 80 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 81 | copies of the Software, and to permit persons to whom the Software is 82 | furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in 85 | all copies or substantial portions of the Software. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 88 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 89 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 90 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 91 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 92 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 93 | THE SOFTWARE. 94 | -------------------------------------------------------------------------------- /example/js/directives/nav.js: -------------------------------------------------------------------------------- 1 | app 2 | .directive('navScroller', function($compile, $rootScope) { 3 | return { 4 | restrict: "A", 5 | require: 'navScroller', 6 | templateUrl: 'partials/nav-scroller.html', 7 | replace: true, 8 | scope: { 9 | "routes": "=navScroller" 10 | }, 11 | controller: function($scope, $location) { 12 | var items = [], current, deltaY = 0, self = this, deltaThreshold = 1000, lastkey; 13 | 14 | bind(); 15 | 16 | function unbind() { 17 | angular.element(document) 18 | .off('wheel', wheelnav) 19 | .off('keydown keyup', keynav); 20 | } 21 | 22 | function bind() { 23 | angular.element(document) 24 | .bind('wheel', wheelnav) 25 | .bind('keydown keyup', keynav); 26 | } 27 | 28 | function nav(next) { 29 | deltaY = 0; 30 | if (next) { 31 | if (self.$selectRoute(next.$route())) { 32 | $scope.$apply(function() { 33 | $location.path(current.$route()); 34 | }); 35 | unbind(); 36 | setTimeout(bind, 500); 37 | } 38 | } 39 | } 40 | 41 | function keynav(event) { 42 | if (event.type === 'keydown') { 43 | lastkey = event.which; 44 | } else if (event.type === 'keyup' && event.which === lastkey) { 45 | var i = self.$index(current); 46 | if (event.which === 38) { 47 | nav(items[i-1]); 48 | } else if (event.which === 40) { 49 | nav(items[i+1]); 50 | } 51 | } 52 | } 53 | 54 | function wheelnav(event) { 55 | deltaY += event.wheelDeltaY; 56 | var next; 57 | if (items.length) { 58 | var i = self.$index(current); 59 | if (deltaY <= -deltaThreshold) { 60 | nav(next = items[i+1]); 61 | } else if (deltaY >= deltaThreshold) { 62 | nav(items[i-1]); 63 | } 64 | } 65 | } 66 | 67 | this.$index = function(navItem) { 68 | for (var i = 0; i < items.length; ++i) { 69 | if (navItem === items[i]) { 70 | return i; 71 | } 72 | } 73 | return -1; 74 | } 75 | 76 | this.$addItem = function(navItem) { 77 | items.push(navItem); 78 | if ($location.path() === navItem.$route()) { 79 | this.$selectRoute(navItem.$route()); 80 | } 81 | }; 82 | this.$selectRoute = function(route) { 83 | if (current) { 84 | if (route === current.$route()) { 85 | return; 86 | } 87 | current = current.$deselect(); 88 | } 89 | for (var i=0; i < items.length; ++i) { 90 | var item = items[i]; 91 | if (item.$route() === route) { 92 | current = item.$select(); 93 | return true; 94 | } 95 | } 96 | }; 97 | }, 98 | link: function($scope, $element, $attr, self) { 99 | angular.forEach($scope.routes, function(route) { 100 | var description = route; 101 | if (typeof route === 'object') { 102 | description = route.description; 103 | route = route.url; 104 | } 105 | var template = ''; 106 | $compile(template)($scope, function(dom) { 107 | $element.append(dom); 108 | }); 109 | }); 110 | 111 | $rootScope.$on('$routeChangeSuccess', function($event, $route) { 112 | self.$selectRoute($route.$$route.originalPath); 113 | }); 114 | } 115 | }; 116 | }) 117 | .directive('navScrollerItem', function() { 118 | return { 119 | restrict: "A", 120 | require: ['navScrollerItem', '^navScroller'], 121 | templateUrl: 'partials/nav-scroller-item.html', 122 | replace: true, 123 | scope: true, 124 | controller: function($scope, $element, $attrs) { 125 | var route = $scope.route = $attrs.navScrollerItem; 126 | $scope.description = $attrs.navDescription; 127 | this.$route = function() { 128 | return route; 129 | }; 130 | this.$select = function() { 131 | $attrs.$addClass('active'); 132 | return this; 133 | }; 134 | this.$deselect = function() { 135 | $attrs.$removeClass('active'); 136 | } 137 | }, 138 | link: function($scope, $element, $attr, $controllers) { 139 | $controllers[1].$addItem($controllers[0]); 140 | } 141 | }; 142 | }); 143 | -------------------------------------------------------------------------------- /example/partials/video.html: -------------------------------------------------------------------------------- 1 |

html5-video

2 | 3 |

The html5-video directive enables the creation of data-bound videos, with support for 4 | overlaid HTML and custom controls.

5 | 6 |

The following example shows a simple, inline utilization of the directive without any 7 | scope-bound expressions.

8 | 9 |
10 |
11 |
12 |
13 | 28 |
29 |
30 |
31 |
32 |       <video data-html5-video="['http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4',
33 |                                 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.webm',
34 |                                 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.ogv']"
35 |              data-tracks="{
36 |                src: 'captions/bunny-en.vtt',
37 |                type: 'text/vtt',
38 |                kind: 'subtitles',
39 |                srclang: 'en-US'
40 |              }"
41 |              data-preload="auto"
42 |              data-autoplay="true"
43 |              data-controls="true">
44 |       </video>    
45 |
46 |
47 |

The video sources and text tracks may be specified using both the `src` attribute, 48 | and the `html5-video` attribute (including variations on the attribute names based on 49 | the AngularJS Directive normalization rules).

50 | 51 |

Additionally, it is possible to specify sources/tracks in a number of ways:

52 |
    53 |
  • String literals (`data-html5-video="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"`) 54 |
  • Interpolated strings (`data-track="./assets/captions/{{video}}-{{locale}}.vtt"`) (Warning: 55 | AngularJS will not allow complex interpolation expressions for the attributes `href` and `src`) 56 |
  • Variables in $scope (`data-src="videos[$index]"`) 57 |
58 |

Many other attributes are also supported. API reference coming soon!

59 |
60 |
61 | -------------------------------------------------------------------------------- /lib/grunt/index.js: -------------------------------------------------------------------------------- 1 | var bower = require('bower'), 2 | path = require('path'), 3 | fs = require('fs'); 4 | module.exports = function(grunt) { 5 | var _ = grunt.util._, root = grunt.config('root'); 6 | var allFiles = grunt.file.expand(path.join(root, 'src', '*.js')); 7 | var allModules = _.map(allFiles, function(file) { 8 | file = file.replace(path.join(root, 'src'), ''); 9 | file = file.replace(/\.js$/, ''); 10 | return file.replace(/\//g, '.'); 11 | }); 12 | _.forEach(['combinator', 'build'], function(task) { 13 | grunt.registerTask(task, 'Combine components into a single wrapped file', function() { 14 | var files; 15 | var mod = 'media'; 16 | var all = false; 17 | if (this.args.length > 0 && this.args.indexOf('all') < 0) { 18 | var args = this.args; 19 | files = _.sortBy(_.compact(_.map(this.args, function(val) { 20 | val = val.toLowerCase(); 21 | var file = val + '.js'; 22 | 23 | var name = path.join(root, 'src', file); 24 | if (grunt.file.exists(name)) { 25 | if (mod === 'scroll' && args.length < 3) { 26 | mod = val; 27 | } 28 | return name; 29 | } 30 | return false; 31 | }))); 32 | if (_.isEqual(files, allFiles)) { 33 | mod = 'media'; 34 | all = true; 35 | } 36 | } else { 37 | files = allFiles; 38 | all = true; 39 | } 40 | if (!files || files.length < 1) { 41 | return grunt.fatal('Cannot combinate 0 files'); 42 | } 43 | var exports = { 44 | directive: [], 45 | controller: [], 46 | provider: [], 47 | service: [], 48 | factory: [], 49 | constant: [], 50 | value: [] 51 | }; 52 | var processed = _.map(files, function(file) { 53 | var text = grunt.file.read(file); 54 | _.forEach(exports, function(val, key) { 55 | var str = "//@" + key, i = 0; 56 | while ((i = text.indexOf(str, i)) >= 0) { 57 | var from = text.slice(i + str.length), start = text.substr(0, i), match = 58 | /^\s+([a-zA-Z0-9_$]+)(?![\r\n])*(\r\n|[\r\n])+/.exec(from); 59 | if (match) { 60 | val.push(match[1]); 61 | from = from.slice(match[0].length); 62 | } 63 | text = start + from; 64 | } 65 | }); 66 | text = text.replace(/\/\/@exports([\s\S]*?)\/\/@end/g, ""); 67 | return unwrap(text); 68 | }); 69 | var ngMod = mod; 70 | if (ngMod !== 'media') { 71 | ngMod = 'media.' + ngMod; 72 | } 73 | processed.push('\nangular.module(\''+ngMod+'\', [])\n' + _.compact(_.map(exports, 74 | function(val, key) { 75 | var items = _.compact(_.map(val, function(name) { 76 | return ' \'' + name + '\': ' + name; 77 | })); 78 | if (items.length > 0) 79 | return '.' + key + '({\n' + items.join(",\n") + '\n})'; 80 | })).join(grunt.util.normalizelf('\n')) + ';\n'); 81 | 82 | processed.push(processCSS(path.resolve(root, 'css/video.css'))); 83 | 84 | if (mod !== 'media') { 85 | mod = 'media-' + mod; 86 | } else if (!all) { 87 | mod = 'media-custom'; 88 | } 89 | grunt.file.write(path.join(root, 'build', 'ng-' + mod + '.js'), 90 | wrap(processed.join(grunt.util.normalizelf('\n')))); 91 | }); 92 | }); 93 | 94 | function unwrap(text) { 95 | var begin = "\\(function\\(window,\\s* document,\\s* undefined\\) \\{'use\\s+strict';", 96 | end = "\\}\\)\\(window,\\s*document\\);"; 97 | text = text.replace(new RegExp('^\\s*' + begin + '\\s*', 'm'), ''); 98 | text = text.replace(new RegExp('\\s*' + end + '\\s*$', 'm'), ''); 99 | return text; 100 | } 101 | function wrap(text) { 102 | return "(function(window, document, undefined) {'use strict';\n\n" + 103 | text + "})(window, document);"; 104 | } 105 | 106 | function processCSS(file) { 107 | var css = fs.readFileSync(file).toString(), js; 108 | 109 | css = css 110 | .replace(/\r?\n/g, '') 111 | .replace(/\/\*.*?\*\//g, '') 112 | .replace(/:\s+/g, ':') 113 | .replace(/\s*\{\s*/g, '{') 114 | .replace(/\s*\}\s*/g, '}') 115 | .replace(/\s*\,\s*/g, ',') 116 | .replace(/\s*\;\s*/g, ';'); 117 | 118 | //escape for js 119 | css = css 120 | .replace(/\\/g, '\\\\') 121 | .replace(/'/g, "\\'") 122 | .replace(/\r?\n/g, '\\n'); 123 | js = "!angular.$$csp() && angular.element(document).find('head').prepend('');\n\n"; 124 | 125 | return js; 126 | } 127 | 128 | grunt.registerTask('bower', 'Install bower packages', function() { 129 | var done = this.async(); 130 | 131 | bower.commands.install() 132 | .on('log', function (result) { 133 | grunt.log.ok('bower: ' + result.id + ' ' + result.data.endpoint.name); 134 | }) 135 | .on('error', grunt.fail.warn.bind(grunt.fail)) 136 | .on('end', done); 137 | }); 138 | 139 | grunt.registerMultiTask('exampleCode', 'Generate preprocessed example code', function() { 140 | var files = grunt.file.expand(this.files); 141 | console.log(this.files); 142 | }); 143 | 144 | grunt.renameTask('copy', 'bower_copy'); 145 | grunt.registerTask('copy', 'Copy assets to build directory', ['bower', 'bower_copy']); 146 | } -------------------------------------------------------------------------------- /example/partials/video.overlay.html: -------------------------------------------------------------------------------- 1 |

html5-video overlay

2 | 3 |

The html5-video directive provides a simple mechanism for HTML overlays

4 | 5 |

This example shows a simple use of the directive with an inline HTML overlay.

6 |

The overlay may contain AngularJS forms which live within the same application, 7 | as no iframe is used.

8 | 9 |
10 |
11 |
12 |
13 | 30 |
31 |
32 |
33 |
34 |       <video data-html5-video="['http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4',
35 |                                 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.webm',
36 |                                 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.ogv']"
37 |              data-track="{
38 |                src: 'assets/captions/bunny-en.vtt',
39 |                type: 'text/vtt',
40 |                kind: 'subtitles',
41 |                srclang: 'en-US',
42 |                label: 'Subtitles'
43 |              }"
44 |              data-controls="true"
45 |              data-width="640"
46 |              data-height="360">
47 | 
48 |         <!-- Overlay code -->
49 |         <p class="shadow">
50 |           It's super easy to overlay HTML over HTML5 media widgest using ng-media!
51 |         </p>
52 |         <img src="/assets/img/anigif.gif" title="Wow!" width="200" />
53 |       </video>
54 |     
55 |
56 |
57 |

While this example showcases a simple inline overlay, it is also possible to overlay a template URL using the `overlay-url` attribute.

58 | 59 |

This could be useful for different things, like adding "remixing" controls similar to the WebMaker 60 | project, or providing an authentication form, or even just a basic watermark for your webapp.

61 | 62 |

You might have lots of different uses for it, and it's easy to accomplish

63 |
64 |
65 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var util = require('./lib/grunt'); 2 | module.exports = function(grunt) { 3 | grunt.initConfig({ 4 | root: __dirname, 5 | 6 | 7 | bower_copy: { 8 | bower: { 9 | files: [{ 10 | expand: true, 11 | cwd: 'bower_components/angular', 12 | src: ['angular.js'], 13 | dest: 'build/js' 14 | }, { 15 | expand: true, 16 | cwd: 'bower_components/angular-animate', 17 | src: 'angular-animate.js', 18 | dest: 'build/js' 19 | }, { 20 | expand: true, 21 | cwd: 'bower_components/angular-route', 22 | src: ['angular-route.js'], 23 | dest: 'build/js' 24 | }, { 25 | expand: true, 26 | cwd: 'bower_components/underscore.string/lib', 27 | src: ['underscore.string.js'], 28 | dest: 'build/js' 29 | }] 30 | }, 31 | example: { 32 | files: [{ 33 | expand: true, 34 | cwd: 'example', 35 | src: ['**/*.html', '**/*.tpl'], 36 | dest: 'build' 37 | }] 38 | }, 39 | example_css: { 40 | files: [{ 41 | expand: true, 42 | cwd: 'example', 43 | src: ['assets/css/*.css'], 44 | dest: 'build' 45 | }] 46 | }, 47 | example_img: { 48 | files: [{ 49 | expand: true, 50 | cwd: 'example', 51 | src: ['assets/img/**/*'], 52 | dest: 'build' 53 | }] 54 | }, 55 | example_assets: { 56 | files: [{ 57 | expand: true, 58 | cwd: 'example', 59 | src: ['assets/**/*', '!example/assets/img/**/*', '!example/assets/css/**/*'], 60 | dest: 'build' 61 | }] 62 | } 63 | }, 64 | 65 | 66 | concat: { 67 | example_js: { 68 | src: ['example/js/app.js', 'example/js/**/*.js'], 69 | dest: 'build/js/app.js' 70 | } 71 | }, 72 | 73 | 74 | connect: { 75 | server: { 76 | options: { 77 | livereload: true, 78 | keepalive: true, 79 | base: 'build', 80 | port: process.env.PORT || 8000 81 | }, 82 | } 83 | }, 84 | 85 | 86 | exampleCode: { 87 | code: { 88 | files: [{ 89 | src: ['example/views/code/**/*.code'], 90 | dest: 'build/js/examplecode.js' 91 | }] 92 | } 93 | }, 94 | 95 | 96 | "ghPages": { 97 | options: { 98 | base: "build", 99 | branch: "gh-pages", 100 | repo: "https://github.com/caitp/ng-media.git" 101 | }, 102 | src: ["**/*"] 103 | }, 104 | 105 | 106 | jshint: { 107 | options: { 108 | curly: true, 109 | eqeqeq: true, 110 | eqnull: true, 111 | browser: true, 112 | globals: { 113 | angular: true 114 | }, 115 | }, 116 | lib: { 117 | src: ['src/**/*.js'], 118 | }, 119 | test: { 120 | src: ['test/**/*.js'], 121 | options: { 122 | globals: { 123 | module: true, 124 | inject: true, 125 | expect: true 126 | } 127 | } 128 | } 129 | }, 130 | 131 | 132 | karma: { 133 | options: { 134 | configFile: 'karma.conf.js' 135 | }, 136 | watch: { 137 | background: true 138 | }, 139 | continuous: { 140 | singleRun: true 141 | }, 142 | jenkins: { 143 | singleRun: true, 144 | colors: false, 145 | reporter: ['dots', 'junit'], 146 | browsers: [ 147 | 'Chrome', 148 | 'ChromeCanary', 149 | 'Firefox', 150 | 'Opera', 151 | '/Users/jenkins/bin/safari.sh', 152 | '/Users/jenkins/bin/ie9.sh' 153 | ] 154 | }, 155 | travis: { 156 | singleRun: true, 157 | browsers: ['PhantomJS', 'Firefox'] 158 | } 159 | }, 160 | 161 | 162 | parallel: { 163 | server: { 164 | tasks: [{ 165 | grunt: true, 166 | args: ['watch'] 167 | }, { 168 | grunt: true, 169 | args: ['connect:server'] 170 | }] 171 | } 172 | }, 173 | 174 | 175 | watch: { 176 | example_html: { 177 | files: ['example/**/*.html', 'example/**/*.tpl'], 178 | tasks: ['bower_copy:example'], 179 | options: { 180 | livereload: true 181 | } 182 | }, 183 | example_css: { 184 | files: ['example/assets/**/*.css'], 185 | tasks: ['bower_copy:example_css'], 186 | options: { 187 | livereload: true 188 | } 189 | }, 190 | example_js: { 191 | files: ['example/**/*.js'], 192 | tasks: ['concat:example_js'], 193 | options: { 194 | livereload: true 195 | } 196 | }, 197 | example_img: { 198 | files: ['example/assets/img/**/*'], 199 | tasks: ['copy:example_img'], 200 | options: { 201 | livereload: true 202 | } 203 | }, 204 | example_assets: { 205 | files: ['example/assets/**/*', '!example/assets/img/**/*', '!example/assets/css/**/*'], 206 | tasks: ['copy:example_assets'], 207 | options: { 208 | livereload: true 209 | } 210 | }, 211 | src: { 212 | files: ['src/**/*.js'], 213 | tasks: ['build'], 214 | options: { 215 | livereload: true 216 | } 217 | } 218 | } 219 | }); 220 | 221 | [ 222 | 'grunt-contrib-concat', 223 | 'grunt-contrib-connect', 224 | 'grunt-contrib-copy', 225 | 'grunt-contrib-jshint', 226 | 'grunt-contrib-watch', 227 | 'grunt-gh-pages', 228 | 'grunt-karma', 229 | 'grunt-parallel' 230 | ].forEach(grunt.loadNpmTasks); 231 | 232 | grunt.loadTasks('lib/grunt'); 233 | 234 | grunt.renameTask('gh-pages', 'ghPages'); 235 | grunt.registerTask('default', ['bower', 'jshint', 'build', 'test']); 236 | grunt.registerTask('example', 'Build example files', ['build', 'bower_copy', 'concat']); 237 | grunt.registerTask('gh-pages', 'Push gh-pages site', ['example', 'ghPages']); 238 | grunt.registerTask('server', 'Run development server', ['example', 'parallel:server']); 239 | grunt.registerTask('test', 'Run tests', ['bower', 'karma:continuous']); 240 | }; 241 | -------------------------------------------------------------------------------- /example/assets/css/ng-media.example.css: -------------------------------------------------------------------------------- 1 | .clearfix:after { 2 | content: "."; 3 | display: block; 4 | clear: both; 5 | visibility: hidden; 6 | line-height: 0; 7 | height: 0; 8 | } 9 | 10 | .clearfix { 11 | display: inline-block; 12 | } 13 | 14 | html[xmlns] .clearfix { 15 | display: block; 16 | } 17 | 18 | * html .clearfix { 19 | height: 1%; 20 | } 21 | 22 | body { 23 | background-color: #000; 24 | color: #fff; 25 | font-family: Arial, Helvetica, sans-serif; 26 | background: url(../img/bg.jpg) 0 0 no-repeat fixed; 27 | background-size: cover; 28 | overflow: hidden; 29 | } 30 | 31 | main { 32 | position: relative; 33 | display:block!important; 34 | margin: 0 auto; 35 | min-width: 720px; 36 | max-width: 1024px; 37 | } 38 | 39 | #main { 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | margin: 0; 44 | padding: 0; 45 | } 46 | 47 | /* iPhone 2G-4S in landscape */ 48 | @media only screen 49 | and (min-device-width: 320px) 50 | and (max-device-width: 480px) 51 | and (orientation: landscape) { 52 | main { 53 | min-width:320px; 54 | max-width:480px; 55 | } 56 | } 57 | 58 | /* iPhone 2G-4S in portrait */ 59 | @media only screen 60 | and (min-device-width: 320px) 61 | and (max-device-width: 480px) 62 | and (orientation: portrait) { 63 | main { 64 | min-width: 320px; 65 | max-width: 480px; 66 | } 67 | } 68 | 69 | /* iPhone 5 in landscape */ 70 | @media only screen 71 | and (min-device-width: 320px) 72 | and (max-device-width: 568px) 73 | and (orientation: landscape) { 74 | main { 75 | min-width: 320px; 76 | max-width: 568px; 77 | } 78 | } 79 | 80 | /* iPhone 5 in portrait */ 81 | @media only screen 82 | and (min-device-width: 320px) 83 | and (max-device-width: 568px) 84 | and (orientation: portrait) { 85 | main { 86 | min-width: 320px; 87 | max-width: 568px; 88 | } 89 | } 90 | 91 | /* iPad Portrait */ 92 | @media only screen 93 | and (min-device-width: 768px) 94 | and (max-device-width: 1024px) 95 | and (orientation: portrait) { 96 | main { 97 | min-width: 768px; 98 | max-width: 1024px; 99 | } 100 | } 101 | 102 | /* iPad Landscape */ 103 | @media only screen 104 | and (min-device-width: 768px) 105 | and (max-device-width: 1024px) 106 | and (orientation: landscape) { 107 | main { 108 | min-width: 768px; 109 | max-width: 1024px; 110 | } 111 | } 112 | 113 | /* Desktop */ 114 | @media only screen 115 | and (min-width: 768px) { 116 | main { 117 | min-width: 768px; 118 | max-width: 768px; 119 | } 120 | #main { 121 | float: left; 122 | min-height: 1px; 123 | min-width: 752px; 124 | max-width: 752px; 125 | } 126 | 127 | .nav-scroller { 128 | margin-top: 5em; 129 | min-width: 16px; 130 | max-width: 16px; 131 | float: right; 132 | } 133 | } 134 | 135 | .nav-scroller-item { 136 | position: relative; 137 | } 138 | 139 | .nav-scroller-item a { 140 | text-decoration: none; 141 | color: #fff; 142 | } 143 | 144 | .nav-scroller-item.active a:before, .nav-scroller-item.active a:hover:before { 145 | color: #3AA6D0; 146 | } 147 | 148 | .nav-scroller-item a:before { 149 | display: inline-block; 150 | font-family: FontAwesome; 151 | font-style: normal; 152 | font-weight: normal; 153 | line-height: 1; 154 | -webkit-font-smoothing: antialiased; 155 | -moz-osx-font-smoothing: grayscale; 156 | -webkit-stroke-width: 5.3px; 157 | -webkit-stroke-color: #FFFFFF; 158 | -webkit-fill-color: #FFFFFF; 159 | text-shadow: 1px 0px 20px yellow; 160 | content: "\f10c"; 161 | } 162 | .nav-scroller-item.active a:before { 163 | content: "\f111"; 164 | } 165 | 166 | .nav-scroller-item > p { 167 | display: none; 168 | } 169 | 170 | .nav-scroller-item:hover > p { 171 | display: block; 172 | position: absolute; 173 | top: -20px; 174 | left: -200px; 175 | background-color: #fff; 176 | color: #000; 177 | min-width: 180px; 178 | max-width: 180px; 179 | padding: .5em; 180 | border-radius: 4px; 181 | -webkit-box-shadow: 0px 3px 3px 3px #222; 182 | -moz-box-shadow: 0px 3px 3px 3px #222; 183 | -o-box-shadow: 0px 3px 3px 3px #222; 184 | box-shadow: 0px 3px 3px 3px #222; 185 | } 186 | 187 | .nav-switcher { 188 | display: inline-block; 189 | border-radius: 4px; 190 | padding: 0; 191 | -webkit-box-shadow: 0px 3px 3px 3px #222; 192 | -moz-box-shadow: 0px 3px 3px 3px #222; 193 | -ms-box-shadow: 0px 3px 3px 3px #222; 194 | -o-box-shadow: 0px 3px 3px 3px #222; 195 | box-shadow: 0px 3px 3px 3px #222; 196 | } 197 | 198 | .nav-switcher-item { 199 | display: inline-block; 200 | background-color: none; 201 | list-style-type: none; 202 | padding: .5em; 203 | border-right: 1px solid rgba(255, 255, 255, .5); 204 | } 205 | 206 | .nav-switcher-item:first-child { 207 | border-radius: 4px 0 0 4px; 208 | } 209 | .nav-switcher-item:last-child { 210 | border-radius: 0 4px 4px 0; 211 | border-right: none; 212 | } 213 | 214 | .nav-switcher-item:hover { 215 | cursor: pointer; 216 | } 217 | 218 | .nav-switcher-item.active { 219 | background-color: #fff; 220 | color: #3AA6D0; 221 | } 222 | 223 | .nav-switcher-item.active:hover { 224 | cursor: default; 225 | } 226 | 227 | .html5-video-wrapper { 228 | text-align: center; 229 | } 230 | 231 | pre.code { 232 | background-color: #fff; 233 | opacity: .5; 234 | padding: 1em; 235 | border-radius: 4px; 236 | color: #000; 237 | } 238 | 239 | .slide-wrapper { 240 | position: relative; 241 | } 242 | 243 | .slide-down.ng-enter, .slide-down.ng-leave { 244 | -webkit-transition: all ease-out 566ms; 245 | -moz-transition: all ease-out 566ms; 246 | -ms-transition: all ease-out 566ms; 247 | -o-transition: all ease-out 566ms; 248 | transition: all ease-out 566ms; 249 | } 250 | 251 | .slide-up.ng-leave-active, 252 | .slide-down.ng-enter { 253 | -webkit-transform: translateY(-999px); 254 | -moz-transform: translateY(-999px); 255 | -ms-transform: translateY(-999px); 256 | -o-transform: translateY(-999px); 257 | transform: translateY(-999px); 258 | } 259 | 260 | 261 | .slide-up.ng-enter-active, .slide-up.ng-leave, 262 | .slide-down.ng-enter-active, .slide-down.ng-leave { 263 | -webkit-transform: translateY(0); 264 | -moz-transform: translateY(0); 265 | -ms-transform: translateY(0); 266 | -o-transform: translateY(0); 267 | transform: translateY(0); 268 | } 269 | 270 | .slide-up.ng-enter, 271 | .slide-down.ng-leave-active { 272 | -webkit-transform: translateY(999px); 273 | -moz-transform: translateY(999px); 274 | -ms-transform: translateY(999px); 275 | -o-transform: translateY(999px); 276 | transform: translateY(999px); 277 | } 278 | 279 | .slide { 280 | position: absolute; 281 | top: 0; 282 | left: 0; 283 | } 284 | 285 | .slide.ng-animate { 286 | display:block!important; 287 | -webkit-transition: all ease-out 566ms; 288 | -moz-transition: all ease-out 566ms; 289 | -ms-transition: all ease-out 566ms; 290 | -o-transition: all ease-out 566ms; 291 | transition: all ease-out 566ms; 292 | } 293 | 294 | .slide.ng-hide-add-active { 295 | -webkit-transform: translateX(-999px); 296 | -moz-transform: translateX(-999px); 297 | -ms-transform: translateX(-999px); 298 | -o-transform: translateX(-999px); 299 | transform: translateX(-999px); 300 | } 301 | 302 | .slide.ng-hide-remove { 303 | -webkit-transform: translateX(9999px); 304 | -moz-transform: translateX(9999px); 305 | -ms-transform: translateX(9999px); 306 | -o-transform: translateX(9999px); 307 | transform: translateX(9999px); 308 | } 309 | 310 | .slide.ng-hide-remove-active { 311 | -webkit-transform: translateX(0); 312 | -moz-transform: translateX(0); 313 | -ms-transform: translateX(0); 314 | -o-transform: translateX(0); 315 | transform: translateX(0); 316 | } 317 | 318 | .shadow { 319 | -webkit-text-shadow: 0px 0px 8px rgba(0, 0, 0, 1); 320 | -moz-text-shadow: 0px 0px 8px rgba(0, 0, 0, 1); 321 | -o-text-shadow: 0px 0px 8px rgba(0, 0, 0, 1); 322 | -ms-text-shadow: 0px 0px 8px rgba(0, 0, 0, 1); 323 | text-shadow: 0px 0px 8px rgba(0, 0, 0, 1); 324 | } -------------------------------------------------------------------------------- /src/video.js: -------------------------------------------------------------------------------- 1 | //@controller html5VideoController 2 | var html5VideoController = [ 3 | '$scope', 4 | '$injector', 5 | '$attr', 6 | 'directiveName', 7 | 'element', 8 | 'video', 9 | 'overlay', function($scope, $injector, $attr, directiveName, element, video, overlay) { 10 | var $parse = $injector.get('$parse'), 11 | $interpolate = $injector.get('$interpolate'), 12 | $http = $injector.get('$http'), 13 | $templateCache = $injector.get('$templateCache'), 14 | $compile = $injector.get('$compile'); 15 | 16 | var exprs = { 17 | track: $parse($attr.track || ''), 18 | width: $parse($attr.width || ''), 19 | height: $parse($attr.height || ''), 20 | poster: $interpolate($attr.poster || '', false), 21 | preload: $parse($attr.preload || ''), 22 | autoplay: $interpolate($attr.autoplay || '', false) 23 | }; 24 | 25 | var _src, 26 | _track, 27 | _poster, 28 | _width, 29 | _height, 30 | _autoplay, 31 | _preload, 32 | changeDetected = 0, 33 | srcChanging = false, 34 | srcChanged = false, 35 | trackChanged = false, 36 | dimensionsChanged = false, 37 | posterChanged = false, 38 | autoplayChanged = false, 39 | preloadChanged = false, 40 | _sources = [], 41 | _tracks = []; 42 | 43 | if (angular.isDefined($attr.controlsUrl) && $attr.controlsUrl.length) { 44 | $attr.$set('controls', undefined); 45 | var url; 46 | try { 47 | url = $scope.$eval($attr.controlsUrl); 48 | } catch (e) {} 49 | 50 | if (typeof url === 'undefined') { 51 | url = $interpolate($attr.controlsUrl, false)($scope); 52 | } 53 | 54 | var videoCtrl = this; 55 | $http.get(url, { cache: true }).then(function(res) { 56 | var controlScope = $scope.$new(); 57 | controlScope.video = videoCtrl; 58 | $compile(res.data)(controlScope, function(dom) { 59 | angular.element(element).append(dom); 60 | }); 61 | }); 62 | } 63 | 64 | if (angular.isDefined($attr.controls) && ['false', '0'].indexOf($attr.controls) < 0) { 65 | video.controls = true; 66 | } 67 | 68 | var $videoWatcher = function() { 69 | var src, 70 | track, 71 | width = exprs.width($scope), 72 | height = exprs.height($scope), 73 | poster = exprs.poster($scope), 74 | autoplay = !!exprs.autoplay($scope), 75 | preload = exprs.preload($scope), 76 | srcSource, 77 | trackSource; 78 | 79 | try { 80 | srcSource = $attr.src || $attr[directiveName] || ''; 81 | src = $scope.$eval(srcSource); 82 | } catch (e) {} 83 | if (typeof src === 'undefined' && srcSource.length) { 84 | src = $interpolate(srcSource, false)($scope); 85 | } 86 | 87 | if (preload === false) { 88 | preload = 'none'; 89 | } else if (preload === true) { 90 | preload = 'auto'; 91 | } 92 | 93 | try { 94 | trackSource = $attr.track || ''; 95 | track = $scope.$eval(trackSource); 96 | } catch (e) {} 97 | 98 | if (typeof track === 'undefined' && trackSource.length) { 99 | track = $interpolate(trackSource, false)($scope); 100 | } 101 | 102 | if (!angular.equals(src, _src)) { 103 | ++changeDetected; 104 | _src = src; 105 | srcChanged = true; 106 | } 107 | 108 | if (!angular.equals(track, _track)) { 109 | ++changeDetected; 110 | _track = track; 111 | trackChanged = true; 112 | } 113 | 114 | if (width !== _width) { 115 | ++changeDetected; 116 | _width = width; 117 | dimensionsChanged = true; 118 | } 119 | 120 | if (height !== _height) { 121 | ++changeDetected; 122 | _height = height; 123 | dimensionsChanged = true; 124 | } 125 | 126 | if (poster !== _poster) { 127 | ++changeDetected; 128 | _poster = poster; 129 | posterChanged = true; 130 | } 131 | 132 | if (autoplay !== _autoplay) { 133 | ++changeDetected; 134 | _autoplay = !!autoplay; 135 | autoplayChanged = true; 136 | } 137 | 138 | if (preload !== _preload) { 139 | ++changeDetected; 140 | _preload = preload; 141 | preloadChanged = true; 142 | } 143 | return changeDetected; 144 | }; 145 | 146 | var updateSrc = function(sources) { 147 | angular.forEach(_sources, function(src, i) { 148 | (src.parentNode || src.parentElement).removeChild(src); 149 | }); 150 | _sources = []; 151 | var last = video.firstChild; 152 | angular.forEach(sources, function(_src, i) { 153 | var source = document.createElement('source'), src = _src; 154 | if (typeof _src === 'object' && _src) { 155 | source.type = _src.type; 156 | source.media = _src.media; 157 | src = _src.src; 158 | } 159 | if (!src) { 160 | source = null; 161 | } else { 162 | source.src = src; 163 | _sources.push(source); 164 | video.insertBefore(source, last); 165 | last = last && last.nextSibling; 166 | } 167 | }); 168 | }; 169 | 170 | var updateTrack = function(tracks) { 171 | angular.forEach(_tracks, function(track, i) { 172 | (track.parentNode || track.parentElement).removeChild(track); 173 | }); 174 | _tracks.length = 0; 175 | angular.forEach(tracks, function(_src, i) { 176 | var track = document.createElement('track'), src = _src; 177 | if (typeof _src === 'object' && _src) { 178 | track.kind = _src.kind || 'subtitles'; 179 | track.charset = _src.charset; 180 | track.srclang = _src.srclang; 181 | track.label = _src.label; 182 | 183 | track.type = _src.type; 184 | track.media = _src.media; 185 | src = _src.src; 186 | } else { 187 | track.kind = 'subtitles'; 188 | } 189 | if (!src) { 190 | track = null; 191 | } else { 192 | track.src = src; 193 | _tracks.push(track); 194 | video.insertBefore(track, null); 195 | } 196 | }); 197 | }; 198 | 199 | var $videoUpdate = function() { 200 | if (srcChanged) { 201 | updateSrc(angular.isArray(_src) ? _src : [_src]); 202 | srcChanging = true; 203 | srcChanged = false; 204 | } 205 | 206 | if (trackChanged) { 207 | updateTrack(angular.isArray(_track) ? _track : [_track]); 208 | trackChanged = false; 209 | } 210 | 211 | if (dimensionsChanged) { 212 | video.width = _width; 213 | video.height = _height; 214 | dimensionsChanged = false; 215 | } 216 | 217 | if (posterChanged) { 218 | video.poster = _poster; 219 | posterChanged = false; 220 | } 221 | 222 | if (autoplayChanged) { 223 | video.autoplay = _autoplay; 224 | autoplayChanged = false; 225 | } 226 | 227 | if (preloadChanged) { 228 | video.preload = _preload; 229 | preloadChanged = false; 230 | } 231 | }; 232 | 233 | $scope.$watch($videoWatcher, $videoUpdate); 234 | 235 | this.$play = function() { 236 | if (srcChanging) { 237 | video.load(); 238 | srcChanging = false; 239 | } 240 | video.play(); 241 | }; 242 | 243 | this.$pause = function() { 244 | video.pause(); 245 | }; 246 | 247 | this.$paused = function() { 248 | return video.paused; 249 | }; 250 | 251 | this.$playbackRate = function(rate) { 252 | if (arguments.length) { 253 | video.playbackRate = rate; 254 | return this; 255 | } else { 256 | return video.playbackRate; 257 | } 258 | }; 259 | 260 | this.$volume = function(volume) { 261 | if (arguments.length) { 262 | video.volume = volume; 263 | return this; 264 | } else { 265 | return video.volume; 266 | } 267 | }; 268 | 269 | this.$mute = function(setting) { 270 | if (arguments.length) { 271 | video.muted = !!setting; 272 | return this; 273 | } else { 274 | return video.muted; 275 | } 276 | }; 277 | }]; 278 | 279 | //@directive html5Video 280 | var html5Video = ['$controller', '$interpolate', '$compile', '$http', '$templateCache', 281 | function($controller, $interpolate, $compile, $http, $templateCache) { 282 | return { 283 | restrict: "A", 284 | transclude: true, 285 | template: '
', 286 | replace: true, 287 | compile: function(element, attr) { 288 | var overlayUrl = attr.overlayUrl; 289 | return function($scope, $element, $attr, undefined, $transclude) { 290 | var video = document.createElement('video'); 291 | var attrOverlay; 292 | var $overlay = angular.element('
'); 293 | if (overlayUrl) { 294 | $http.get($interpolate(overlayUrl, false)($scope), { cache: $templateCache }) 295 | .success(function(tpl) { 296 | $overlay.html(tpl); 297 | $compile($overlay)($scope, function(node) { 298 | $element.append(node); 299 | }); 300 | }); 301 | attrOverlay = true; 302 | } 303 | 304 | $element.data('$html5VideoController', $controller('html5VideoController', { 305 | $scope: $scope, 306 | $attr: $attr, 307 | directiveName: 'html5Video', 308 | element: $element[0], 309 | video: video, 310 | overlay: $overlay[0] 311 | })); 312 | $element.append(video); 313 | if (!attrOverlay) { 314 | $element.append($overlay); 315 | $transclude($scope, function(dom) { 316 | $overlay.append(dom); 317 | }); 318 | } 319 | }; 320 | } 321 | }; 322 | }]; 323 | -------------------------------------------------------------------------------- /test/video.spec.js: -------------------------------------------------------------------------------- 1 | describe('html5Video', function() { 2 | var element; 3 | 4 | 5 | afterEach(function() { 6 | element.remove(); 7 | element = null; 8 | }); 9 | 10 | 11 | describe('overlay', function() { 12 | it('should match the dimensions of video', inject(function($compile, $document, $rootScope) { 13 | element = $compile('
')($rootScope, function(dom) { 14 | $document.find('body').prepend(dom); 15 | }); 16 | $rootScope.$digest(); 17 | var overlay = element.children('div')[0]; 18 | expect(overlay.offsetWidth).toEqual(640); 19 | expect(overlay.offsetHeight).toEqual(360); 20 | })); 21 | 22 | 23 | it('should have directive child nodes inserted into it', inject(function($compile, $rootScope) { 24 | element = $compile('

Transcluded Node

')($rootScope); 25 | $rootScope.$digest(); 26 | expect(element.find('p').length).toEqual(1); 27 | expect(element.find('p').text()).toEqual('Transcluded Node'); 28 | })); 29 | }); 30 | 31 | 32 | describe('controls', function() { 33 | angular.forEach(['', 'true', '1', 'NaN', 'null', 'undefined'], function(value) { 34 | it('should set video.controls=true if controls="' + value + '"', inject(function($compile, $rootScope) { 35 | element = $compile('
')($rootScope); 36 | $rootScope.$digest(); 37 | expect(element.find('video').prop('controls')).toEqual(true); 38 | })); 39 | }); 40 | 41 | 42 | angular.forEach(['0', 'false'], function(value) { 43 | it('should set video.controls=false if controls="' + value + '"', inject(function($compile, $rootScope) { 44 | element = $compile('
')($rootScope); 45 | $rootScope.$digest(); 46 | expect(element.find('video').prop('controls')).toBeFalsy(); 47 | })); 48 | }); 49 | 50 | 51 | it('should set video.controls=false if controls-url supplied', inject(function($compile, $httpBackend, $rootScope) { 52 | $httpBackend.expectGET('ctrl.tpl').respond(''); 53 | element = $compile('
')($rootScope); 54 | $rootScope.$digest(); 55 | expect(element.find('video').prop('controls')).toBeFalsy(); 56 | })); 57 | 58 | 59 | it('should inject html5VideoController into controls template as `video`', inject(function($compile, $document, $httpBackend, $rootScope) { 60 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 61 | element = $compile('
')($rootScope, function(dom) { 62 | $document.find('body').prepend(dom); 63 | }); 64 | $httpBackend.flush(); 65 | $rootScope.$digest(); 66 | var controls = angular.element(document.getElementById('controls')); 67 | expect(controls.scope().video).toEqual(element.controller('html5Video')); 68 | })); 69 | 70 | 71 | it('should $compile directives in controls-url template', inject(function($compile, $document, $httpBackend, $rootScope) { 72 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 73 | element = $compile('
')($rootScope, function(dom) { 74 | $document.find('body').prepend(dom); 75 | }); 76 | $httpBackend.flush(); 77 | $rootScope.$digest(); 78 | var controller = element.controller('html5Video'), 79 | video = element.find('video')[0]; 80 | spyOn(controller, '$play'); 81 | var controls = angular.element(document.getElementById('controls')); 82 | controls.triggerHandler('click'); 83 | expect(controller.$play).toHaveBeenCalled(); 84 | })); 85 | 86 | 87 | it('should play video when video.$play() called', inject(function($compile, $rootScope, $httpBackend, $document) { 88 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 89 | element = $compile('
')($rootScope, function(dom) { 90 | $document.find('body').prepend(dom); 91 | }); 92 | $httpBackend.flush(); 93 | $rootScope.$digest(); 94 | 95 | var video = element.find('video')[0]; 96 | spyOn(video, 'play'); 97 | 98 | var controls = angular.element(document.getElementById('controls')); 99 | controls.triggerHandler('click'); 100 | 101 | expect(video.play).toHaveBeenCalled(); 102 | })); 103 | 104 | 105 | it('should load video when video.$play() called with new source', inject(function($compile, $rootScope, $httpBackend, $document) { 106 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 107 | element = $compile('
')($rootScope, function(dom) { 108 | $document.find('body').prepend(dom); 109 | }); 110 | $httpBackend.flush(); 111 | $rootScope.$digest(); 112 | 113 | var video = element.find('video')[0]; 114 | spyOn(video, 'load'); 115 | 116 | $rootScope.source = "test.vp8"; 117 | $rootScope.$digest(); 118 | 119 | var controls = angular.element(document.getElementById('controls')); 120 | controls.triggerHandler('click'); 121 | 122 | expect(video.load).toHaveBeenCalled(); 123 | })); 124 | 125 | 126 | it('should pause video when video.$pause() called', inject(function($compile, $rootScope, $httpBackend, $document) { 127 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 128 | element = $compile('
')($rootScope, function(dom) { 129 | $document.find('body').prepend(dom); 130 | }); 131 | $httpBackend.flush(); 132 | $rootScope.$digest(); 133 | 134 | var video = element.find('video')[0]; 135 | video.play(); 136 | spyOn(video, 'pause'); 137 | 138 | var controls = angular.element(document.getElementById('controls')); 139 | controls.triggerHandler('click'); 140 | 141 | expect(video.pause).toHaveBeenCalled(); 142 | })); 143 | 144 | 145 | it('should report playback status when video.$paused() called', inject(function($compile, $rootScope, $httpBackend, $document) { 146 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 147 | element = $compile('
')($rootScope, function(dom) { 148 | $document.find('body').prepend(dom); 149 | }); 150 | $httpBackend.flush(); 151 | $rootScope.$digest(); 152 | 153 | var video = element.find('video')[0]; 154 | var controls = angular.element(document.getElementById('controls')); 155 | var videoCtrl = controls.scope().video; 156 | 157 | expect(videoCtrl.$paused()).toBe(true); 158 | video.play(); 159 | 160 | expect(videoCtrl.$paused()).toBe(false); 161 | 162 | controls.triggerHandler('click'); 163 | expect(videoCtrl.$paused()).toBe(true); 164 | })); 165 | 166 | 167 | it('should report playback rate when video.$playbackRate() called with no arguments', inject(function($compile, $rootScope, $httpBackend, $document) { 168 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 169 | element = $compile('
')($rootScope, function(dom) { 170 | $document.find('body').prepend(dom); 171 | }); 172 | $httpBackend.flush(); 173 | $rootScope.$digest(); 174 | 175 | var video = element.find('video')[0]; 176 | var controls = angular.element(document.getElementById('controls')); 177 | var videoCtrl = controls.scope().video; 178 | 179 | video.play(); 180 | 181 | expect(videoCtrl.$playbackRate()).toBe(1); 182 | })); 183 | 184 | 185 | it('should set playback rate when video.$playbackRate() called with value', inject(function($compile, $rootScope, $httpBackend, $document) { 186 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 187 | element = $compile('
')($rootScope, function(dom) { 188 | $document.find('body').prepend(dom); 189 | }); 190 | $httpBackend.flush(); 191 | $rootScope.$digest(); 192 | 193 | var video = element.find('video')[0]; 194 | var controls = angular.element(document.getElementById('controls')); 195 | var videoCtrl = controls.scope().video; 196 | 197 | video.play(); 198 | videoCtrl.$playbackRate(0.5); 199 | 200 | expect(videoCtrl.$playbackRate()).toBe(0.5); 201 | })); 202 | 203 | 204 | it('should report playback volume when video.$volume() called with no arguments', inject(function($compile, $rootScope, $httpBackend, $document) { 205 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 206 | element = $compile('
')($rootScope, function(dom) { 207 | $document.find('body').prepend(dom); 208 | }); 209 | $httpBackend.flush(); 210 | $rootScope.$digest(); 211 | 212 | var video = element.find('video')[0]; 213 | var controls = angular.element(document.getElementById('controls')); 214 | var videoCtrl = controls.scope().video; 215 | 216 | video.play(); 217 | 218 | expect(videoCtrl.$volume()).toBe(1); 219 | })); 220 | 221 | 222 | it('should set playback volume when video.$volume() called with value', inject(function($compile, $rootScope, $httpBackend, $document) { 223 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 224 | element = $compile('
')($rootScope, function(dom) { 225 | $document.find('body').prepend(dom); 226 | }); 227 | $httpBackend.flush(); 228 | $rootScope.$digest(); 229 | 230 | var video = element.find('video')[0]; 231 | var controls = angular.element(document.getElementById('controls')); 232 | var videoCtrl = controls.scope().video; 233 | 234 | video.play(); 235 | videoCtrl.$volume(0.5); 236 | 237 | expect(videoCtrl.$volume()).toBe(0.5); 238 | })); 239 | 240 | 241 | it('should report muted status when video.$mute() called with no arguments', inject(function($compile, $rootScope, $httpBackend, $document) { 242 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 243 | element = $compile('
')($rootScope, function(dom) { 244 | $document.find('body').prepend(dom); 245 | }); 246 | $httpBackend.flush(); 247 | $rootScope.$digest(); 248 | 249 | var video = element.find('video')[0]; 250 | var controls = angular.element(document.getElementById('controls')); 251 | var videoCtrl = controls.scope().video; 252 | 253 | video.play(); 254 | 255 | expect(videoCtrl.$mute()).toBe(false); 256 | })); 257 | 258 | 259 | it('should mute volume when video.$mute() called with truthy value', inject(function($compile, $rootScope, $httpBackend, $document) { 260 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 261 | element = $compile('
')($rootScope, function(dom) { 262 | $document.find('body').prepend(dom); 263 | }); 264 | $httpBackend.flush(); 265 | $rootScope.$digest(); 266 | 267 | var video = element.find('video')[0]; 268 | var controls = angular.element(document.getElementById('controls')); 269 | var videoCtrl = controls.scope().video; 270 | 271 | video.play(); 272 | videoCtrl.$mute({}); 273 | expect(videoCtrl.$mute()).toBe(true); 274 | videoCtrl.$mute(true); 275 | expect(videoCtrl.$mute()).toBe(true); 276 | videoCtrl.$mute("0"); 277 | expect(videoCtrl.$mute()).toBe(true); 278 | })); 279 | 280 | 281 | it('should unmute volume when video.$mute() called with falsy value', inject(function($compile, $rootScope, $httpBackend, $document) { 282 | $httpBackend.expectGET('ctrl.tpl').respond('
'); 283 | element = $compile('
')($rootScope, function(dom) { 284 | $document.find('body').prepend(dom); 285 | }); 286 | $httpBackend.flush(); 287 | $rootScope.$digest(); 288 | 289 | var video = element.find('video')[0]; 290 | var controls = angular.element(document.getElementById('controls')); 291 | var videoCtrl = controls.scope().video; 292 | 293 | video.play(); 294 | videoCtrl.$mute(false); 295 | expect(videoCtrl.$mute()).toBe(false); 296 | videoCtrl.$mute(null); 297 | expect(videoCtrl.$mute()).toBe(false); 298 | videoCtrl.$mute(""); 299 | expect(videoCtrl.$mute()).toBe(false); 300 | videoCtrl.$mute(undefined); 301 | expect(videoCtrl.$mute()).toBe(false); 302 | })); 303 | }); 304 | 305 | 306 | describe('sources', function() { 307 | angular.forEach(['html5-video', 'src'], function(attr) { 308 | angular.forEach({ 309 | 'as a string literal': { 310 | value: "test.vp9", 311 | expected: [{ src: "test.vp9$" }] 312 | }, 313 | 'as an interpolated string': { 314 | setup: function(scope) { scope.source = "test.vp9"; }, 315 | value: "{{source}}", 316 | expected: [{ src: "test.vp9$" }] 317 | }, 318 | 'as a scope value': { 319 | setup: function(scope) { scope.test = { vp9: "test.vp8" }; }, 320 | value: "test.vp9", 321 | expected: [{ src: "test.vp8$" }] 322 | }, 323 | 'as an object': { 324 | value: "{src: 'test.webm', type: 'video/webm' }", 325 | expected: [ { src: "test.webm$", type: "video/webm" }] 326 | }, 327 | 'as an array of strings': { 328 | value: "['test.vp8', 'test.ts']", 329 | expected: [ { src: "test.vp8$" }, { src: "test.ts" }] 330 | }, 331 | 'as an array of objects': { 332 | value: "[{src: 'test.webm', type: 'video/webm' }, {src: 'test.mp4', type: 'video/mp4' }]", 333 | expected: [ { src: "test.webm$", type: "video/webm" }, 334 | { src: "test.mp4$", type: "video/mp4" }] 335 | } 336 | }, function(test, desc) { 337 | it('should support specifying sources using the `' + attr + '` attribute', inject(function($compile, $rootScope) { 338 | if (test.setup) { 339 | test.setup($rootScope); 340 | } 341 | var attrs = attr === 'src' ? 'data-html5-video src="' + test.value + '"' : 'data-html5-video="' + test.value + '"'; 342 | element = $compile('
')($rootScope); 343 | $rootScope.$digest(); 344 | var sources = element.find('source'); 345 | expect(sources.length).toEqual(test.expected.length); 346 | angular.forEach(test.expected, function(e, i) { 347 | var source = sources.eq(i); 348 | if (typeof e.src === 'string') { 349 | expect(source.attr('src')).toMatch(e.src); 350 | } 351 | if (typeof e.type === 'string') { 352 | expect(source.attr('type')).toEqual(e.type); 353 | } 354 | }); 355 | })); 356 | }); 357 | }); 358 | 359 | 360 | it('should update sources when interpolated value changes', inject(function($compile, $rootScope) { 361 | $rootScope.source = "test1.mp4"; 362 | element = $compile('
')($rootScope); 363 | $rootScope.$digest(); 364 | expect(element.find('source').length).toEqual(1); 365 | expect(element.find('source').prop('src')).toMatch(/test1\.mp4$/); 366 | $rootScope.$apply(function() { $rootScope.source = 'test2.vp8'; }); 367 | expect(element.find('source').length).toEqual(1); 368 | expect(element.find('source').prop('src')).toMatch(/test2\.vp8$/); 369 | })); 370 | 371 | 372 | it('should update sources when parsed value changes', inject(function($compile, $rootScope) { 373 | $rootScope.source = "test1.mp4"; 374 | element = $compile('
')($rootScope); 375 | $rootScope.$digest(); 376 | expect(element.find('source').length).toEqual(1); 377 | expect(element.find('source').prop('src')).toMatch(/test1\.mp4$/); 378 | $rootScope.$apply(function() { $rootScope.source = 'test2.vp8'; }); 379 | expect(element.find('source').length).toEqual(1); 380 | expect(element.find('source').prop('src')).toMatch(/test2\.vp8$/); 381 | })); 382 | 383 | 384 | it('should remove falsy sources from sources', inject(function($compile, $rootScope) { 385 | $rootScope.sources = ['', null, false, undefined]; 386 | element = $compile('
')($rootScope); 387 | $rootScope.$digest(); 388 | expect(element.find('source').length).toBe(0); 389 | })); 390 | }); 391 | 392 | describe('tracks', function() { 393 | angular.forEach({ 394 | 'as a string literal': { 395 | value: "test.vtt", 396 | expected: [{ src: "test.vtt$", kind: "subtitles" }] 397 | }, 398 | 'as an interpolated string': { 399 | setup: function(scope) { scope.source = "test.vtt"; }, 400 | value: "{{source}}", 401 | expected: [{ src: "test.vtt$", kind: "subtitles" }] 402 | }, 403 | 'as a scope value': { 404 | setup: function(scope) { scope.test = { vtt: "parsed.vtt" }; }, 405 | value: "test.vtt", 406 | expected: [{ src: "parsed.vtt$", kind: "subtitles" }] 407 | }, 408 | 'as an object': { 409 | value: "{src: 'test.vtt', type: 'text/vtt', kind: 'captions' }", 410 | expected: [ { src: "test.vtt$", type: "text/vtt", kind: "captions" }] 411 | }, 412 | 'as an array of strings': { 413 | value: "['test1.vtt', 'test2.vtt']", 414 | expected: [ { src: "test1.vtt$", kind: "subtitles" }, { src: "test2.vtt$", kind: "subtitles" }] 415 | }, 416 | 'as an array of objects': { 417 | value: "[{src: 'test.vtt', type: 'text/vtt', kind: 'captions' }, {src: 'test2.vtt', type: 'text/vtt' }]", 418 | expected: [ { src: "test.vtt$", type: "text/vtt", kind: "captions" }, 419 | { src: "test2.vtt$", type: "text/vtt", kind: "subtitles" }] 420 | } 421 | }, function(test, desc) { 422 | it('should support specifying tracks using the `track` attribute', inject(function($compile, $rootScope) { 423 | if (test.setup) { 424 | test.setup($rootScope); 425 | } 426 | var attrs = 'data-html5-video="test.mp4" track="' + test.value + '"'; 427 | element = $compile('
')($rootScope); 428 | $rootScope.$digest(); 429 | var tracks = element.find('track'); 430 | expect(tracks.length).toEqual(test.expected.length); 431 | angular.forEach(test.expected, function(e, i) { 432 | var track = tracks.eq(i); 433 | if (typeof e.src === 'string') { 434 | expect(track.prop('src')).toMatch(e.src); 435 | } 436 | if (typeof e.kind === 'string') { 437 | expect(track.prop('kind')).toEqual(e.kind); 438 | } 439 | if (typeof e.type === 'string') { 440 | expect(track.prop('type')).toEqual(e.type); 441 | } 442 | }); 443 | })); 444 | }); 445 | 446 | 447 | it('should update tracks when interpolated value changes', inject(function($compile, $rootScope) { 448 | $rootScope.track = "test1.vtt"; 449 | element = $compile('
')($rootScope); 450 | $rootScope.$digest(); 451 | expect(element.find('track').length).toEqual(1); 452 | expect(element.find('track').prop('src')).toMatch(/test1\.vtt$/); 453 | $rootScope.$apply(function() { $rootScope.track = 'test2.vtt'; }); 454 | expect(element.find('track').length).toEqual(1); 455 | expect(element.find('track').prop('src')).toMatch(/test2\.vtt$/); 456 | })); 457 | 458 | 459 | it('should update tracks when parsed value changes', inject(function($compile, $rootScope) { 460 | $rootScope.track = "test1.vtt"; 461 | element = $compile('
')($rootScope); 462 | $rootScope.$digest(); 463 | expect(element.find('track').length).toEqual(1); 464 | expect(element.find('track').prop('src')).toMatch(/test1\.vtt$/); 465 | $rootScope.$apply(function() { $rootScope.track = 'test2.vtt'; }); 466 | expect(element.find('track').length).toEqual(1); 467 | expect(element.find('track').prop('src')).toMatch(/test2\.vtt$/); 468 | })); 469 | 470 | 471 | it('should remove falsy sources from tracks', inject(function($compile, $rootScope) { 472 | $rootScope.tracks = ['', null, false, undefined]; 473 | element = $compile('
')($rootScope); 474 | $rootScope.$digest(); 475 | expect(element.find('track').length).toBe(0); 476 | })); 477 | 478 | 479 | describe('overlayUrl', function() { 480 | it('should replace inline overlay with template', inject(function($compile, $rootScope, $templateCache) { 481 | $templateCache.put('overlay.html', '

This is overlay from template

'); 482 | element = $compile('
derp
')($rootScope); 483 | $rootScope.$digest(); 484 | expect(element.find('span').length).toBe(0); 485 | expect(element.find('p').length).toBe(1); 486 | expect(element.find('p').text()).toBe('This is overlay from template'); 487 | })); 488 | }); 489 | }); 490 | 491 | 492 | describe('preload', function() { 493 | it('should be databound', inject(function($compile, $rootScope) { 494 | element = $compile('
')($rootScope); 495 | $rootScope.$preload = 'metadata'; 496 | $rootScope.$digest(); 497 | var video = element.find('video')[0]; 498 | 499 | expect(video.preload).toBe('metadata'); 500 | 501 | $rootScope.$preload = 'none'; 502 | $rootScope.$digest(); 503 | 504 | expect(video.preload).toBe('none'); 505 | })); 506 | 507 | 508 | it('should treat `false` as `none`', inject(function($compile, $rootScope) { 509 | element = $compile('
')($rootScope); 510 | $rootScope.$preload = false; 511 | $rootScope.$digest(); 512 | var video = element.find('video')[0]; 513 | 514 | expect(video.preload).toBe('none'); 515 | })); 516 | 517 | 518 | it('should treat `true` as `auto`', inject(function($compile, $rootScope) { 519 | element = $compile('
')($rootScope); 520 | $rootScope.$preload = true; 521 | $rootScope.$digest(); 522 | var video = element.find('video')[0]; 523 | 524 | expect(video.preload).toBe('auto'); 525 | })); 526 | }); 527 | }); 528 | --------------------------------------------------------------------------------