├── 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 |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 |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 |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
13 | 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 [](https://travis-ci.org/caitp/ng-media) [](https://david-dm.org/caitp/ng-media#info=devDependencies) [](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 |14 | HTML, including custom video controls, can be overlayed easily over the 15 | video frame. 16 |
17 |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 | 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 |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 | 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
This is overlay from template
'); 482 | element = $compile('