Permalink
Browse files

@heff added support for fluid widths, aspect ratios, and metadata def…

…aults. closes #1952
  • Loading branch information...
heff committed May 14, 2015
1 parent 6128305 commit 2fc8968002cf2f40128c39699c3ffbaac73fc9ed
Showing with 238 additions and 31 deletions.
  1. +1 −0 CHANGELOG.md
  2. +1 −0 package.json
  3. +34 −3 src/css/components/_layout.scss
  4. +4 −0 src/css/components/_poster.scss
  5. +0 −3 src/js/options.js
  6. +143 −6 src/js/player.js
  7. +3 −0 src/js/tech/html5.js
  8. +52 −19 test/unit/player.js
@@ -29,6 +29,7 @@ CHANGELOG
* @bc-bbay fixed instance where progress bars would go passed 100% ([view](https://github.com/videojs/video.js/pull/2040))
* @eXon began Tech 2.0 work, improved how tech events are handled by the player ([view](https://github.com/videojs/video.js/pull/2057))
* @gkatsev added get and set global options methods ([view](https://github.com/videojs/video.js/pull/2115))
* @heff added support for fluid widths, aspect ratios, and metadata defaults ([view](https://github.com/videojs/video.js/pull/1952))
--------------------
@@ -37,6 +37,7 @@
"browserify-istanbul": "^0.2.1",
"browserify-versionify": "^1.0.4",
"chg": "~0.2.0",
"css": "^2.2.0",
"grunt": "^0.4.4",
"grunt-aws-s3": "^0.12.1",
"grunt-banner": "^0.3.1",
@@ -1,16 +1,22 @@
.video-js {
display: block;
/* inline-block is as close as we get to the video el's display:inline */
display: inline-block;
/* Make video.js videos align top when next to video elements */
vertical-align: top;
box-sizing: border-box;
/* Default to the video element width/height. This will be overridden by
* the source width height unless changed elsewhere. */
width: 300px;
height: 150px;
color: $primary-text;
background-color: $primary-bg;
position: relative;
padding: 0;
/* Start with 10px for base font size so other dimensions can be em based and
easily calculable. */
font-size: $base-font-size;
/* Allow poster to be vertially aligned. */
vertical-align: middle;
/* Provide some basic defaults for fonts */
font-weight: normal;
@@ -37,6 +43,29 @@
box-sizing: inherit;
}
/* Fill the width of the containing element and use padding to create the
desired aspect ratio. Default to 16x9 unless another ratio is given. */
@mixin apply-aspect-ratio($width, $height) {
width: 100%;
max-width: 100%;
height: 0;
padding-top: 100% * ($height/$width);
}
.video-js.vjs-fluid,
.video-js.vjs-16-9 {
@include apply-aspect-ratio(16, 9);
}
.video-js.vjs-4-3 {
@include apply-aspect-ratio(4, 3);
}
.video-js.vjs-fill {
width: 100%;
height: 100%;
}
/* Playback technology elements expand to the width/height of the containing div
<video> or <object> */
.video-js .vjs-tech {
@@ -65,6 +94,8 @@ body.vjs-full-window {
right: 0;
width: 100% !important;
height: 100% !important;
/* Undo any aspect ratio padding for fluid layouts */
padding-top: 0 !important;
}
.video-js.vjs-fullscreen.vjs-user-inactive {
cursor: none;
@@ -1,4 +1,6 @@
.vjs-poster {
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
@@ -10,9 +12,11 @@
right: 0;
bottom: 0;
left: 0;
height: 100%;
}
.vjs-poster img {
display: block;
vertical-align: middle;
margin: 0 auto;
max-height: 100%;
padding: 0;
@@ -17,9 +17,6 @@ export default {
'html5': {},
'flash': {},
// Default of web browser is 300x150. Should rely on source width/height.
'width': 300,
'height': 150,
// defaultVolume: 0.85,
'defaultVolume': 0.00, // The freakin seaguls are driving me crazy!
@@ -222,10 +222,17 @@ class Player extends Component {
// Default state of video is paused
this.addClass('vjs-paused');
// Make box use width/height of tag, or rely on default implementation
// Enforce with CSS since width/height attrs don't work on divs
this.width(this.options_['width'], true); // (true) Skip resize listener on load
this.height(this.options_['height'], true);
// Add a style element in the player that we'll use to set the width/height
// of the player in a way that's still overrideable by CSS, just like the
// video element
this.styleEl_ = document.createElement('style');
el.appendChild(this.styleEl_);
// Pass in the width/height/aspectRatio options which will update the style el
this.width(this.options_['width']);
this.height(this.options_['height']);
this.fluid(this.options_['fluid']);
this.aspectRatio(this.options_['aspectRatio']);
// Lib.insertFirst seems to cause the networkState to flicker from 3 to 2, so
// keep track of the original for later so we can know if the source originally failed
@@ -242,6 +249,129 @@ class Player extends Component {
return el;
}
width(value) {
return this.dimension('width', value);
}
height(value) {
return this.dimension('height', value);
}
dimension(dimension, value) {
let privDimension = dimension + '_';
if (value === undefined) {
return this[privDimension] || 0;
}
if (value === '') {
// If an empty string is given, reset the dimension to be automatic
this[privDimension] = undefined;
} else {
let parsedVal = parseFloat(value);
if (isNaN(parsedVal)) {
Lib.log.error(`Improper value "${value}" supplied for for ${dimension}`);
return this;
}
this[privDimension] = parsedVal;
}
this.updateStyleEl_();
return this;
}
fluid(bool) {
if (bool === undefined) {
return !!this.fluid_;
}
this.fluid_ = !!bool;
if (bool) {
this.addClass('vjs-fluid');
} else {
this.removeClass('vjs-fluid');
}
}
aspectRatio(ratio) {
if (ratio === undefined) {
return this.aspectRatio_;
}
// Check for width:height format
if (!/^\d+\:\d+$/.test(ratio)) {
throw new Error('Improper value suplied for aspect ratio. The format should be width:height, for example 16:9.');
}
this.aspectRatio_ = ratio;
// We're assuming if you set an aspect ratio you want fluid mode,
// because in fixed mode you could calculate width and height yourself.
this.fluid(true);
this.updateStyleEl_();
}
updateStyleEl_() {
let width;
let height;
let aspectRatio;
// The aspect ratio is either used directly or to calculate width and height.
if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
// Use any aspectRatio that's been specifically set
aspectRatio = this.aspectRatio_;
} else if (this.videoWidth()) {
// Otherwise try to get the aspect ratio from the video metadata
aspectRatio = this.videoWidth() + ':' + this.videoHeight();
} else {
// Or use a default. The video element's is 2:1, but 16:9 is more common.
aspectRatio = '16:9';
}
// Get the ratio as a decimal we can use to calculate dimensions
let ratioParts = aspectRatio.split(':');
let ratioMultiplier = ratioParts[1] / ratioParts[0];
if (this.width_ !== undefined) {
// Use any width that's been specifically set
width = this.width_;
} else if (this.height_ !== undefined) {
// Or calulate the width from the aspect ratio if a height has been set
width = this.height_ / ratioMultiplier;
} else {
// Or use the video's metadata, or use the video el's default of 300
width = this.videoWidth() || 300;
}
if (this.height_ !== undefined) {
// Use any height that's been specifically set
height = this.height_;
} else {
// Otherwise calculate the height from the ratio and the width
height = width * ratioMultiplier;
}
let idClass = this.id()+'-dimensions';
// Ensure the right class is still on the player for the style element
this.addClass(idClass);
// Create the width/height CSS
var css = `.${idClass} { width: ${width}px; height: ${height}px; }`;
// Add the aspect ratio CSS for when using a fluid layout
css += `.${idClass}.vjs-fluid { padding-top: ${ratioMultiplier * 100}%; }`;
// Update the style el
if (this.styleEl_.styleSheet){
this.styleEl_.styleSheet.cssText = css;
} else {
this.styleEl_.innerHTML = css;
}
}
/**
* Load the Media Playback Technology (tech)
* Load/Create an instance of playback technology including element and API methods
@@ -323,6 +453,7 @@ class Player extends Component {
this.on(this.tech, 'ratechange', this.handleTechRateChange);
this.on(this.tech, 'volumechange', this.handleTechVolumeChange);
this.on(this.tech, 'texttrackchange', this.onTextTrackChange);
this.on(this.tech, 'loadedmetadata', this.updateStyleEl_);
if (this.controls() && !this.usingNativeControls()) {
this.addTechControlsListeners();
@@ -1922,15 +2053,21 @@ class Player extends Component {
this.tech && this.tech['removeRemoteTextTrack'](track);
}
videoWidth() {
return this.tech && this.tech.videoWidth && this.tech.videoWidth() || 0;
}
videoHeight() {
return this.tech && this.tech.videoHeight && this.tech.videoHeight() || 0;
}
// Methods to add support for
// initialTime: function(){ return this.techCall('initialTime'); },
// startOffsetTime: function(){ return this.techCall('startOffsetTime'); },
// played: function(){ return this.techCall('played'); },
// seekable: function(){ return this.techCall('seekable'); },
// videoTracks: function(){ return this.techCall('videoTracks'); },
// audioTracks: function(){ return this.techCall('audioTracks'); },
// videoWidth: function(){ return this.techCall('videoWidth'); },
// videoHeight: function(){ return this.techCall('videoHeight'); },
// defaultPlaybackRate: function(){ return this.techCall('defaultPlaybackRate'); },
// mediaGroup: function(){ return this.techCall('mediaGroup'); },
// controller: function(){ return this.techCall('controller'); },
@@ -270,6 +270,9 @@ class Html5 extends Tech {
networkState() { return this.el_.networkState; }
readyState() { return this.el_.readyState; }
videoWidth() { return this.el_.videoWidth; }
videoHeight() { return this.el_.videoHeight; }
textTracks() {
if (!this['featuresNativeTextTracks']) {
return super.textTracks();
@@ -6,6 +6,7 @@ import MediaError from '../../src/js/media-error.js';
import Html5 from '../../src/js/tech/html5.js';
import TestHelpers from './test-helpers.js';
import document from 'global/document';
import css from 'css';
q.module('Player', {
'setup': function() {
@@ -152,31 +153,63 @@ test('should asynchronously fire error events during source selection', function
Lib.log.error.restore();
});
test('should set the width and height of the player', function(){
var player = TestHelpers.makePlayer({ width: 123, height: '100%' });
test('should set the width, height, and aspect ratio via a css class', function(){
let player = TestHelpers.makePlayer();
let getStyleText = function(styleEl){
return (styleEl.styleSheet && styleEl.styleSheet.cssText) || styleEl.innerHTML;
};
ok(player.width() === 123);
ok(player.el().style.width === '123px');
ok(player.styleEl_.parentNode === player.el(), 'player has a style element');
ok(!getStyleText(player.styleEl_), 'style element should be empty when the player is given no dimensions');
var fixture = document.getElementById('qunit-fixture');
var container = document.createElement('div');
fixture.appendChild(container);
let rules;
// Player container needs to have height in order to have height
// Don't want to mess with the fixture itself
container.appendChild(player.el());
container.style.height = '1000px';
ok(player.height() === 1000);
function getStyleRules(){
const styleText = getStyleText(player.styleEl_);
const cssAST = css.parse(styleText);
const styleRules = {};
player.dispose();
});
cssAST.stylesheet.rules.forEach(function(ruleAST){
let selector = ruleAST.selectors.join(' ');
styleRules[selector] = {};
let rule = styleRules[selector];
test('should not force width and height', function() {
var player = TestHelpers.makePlayer({ width: 'auto', height: 'auto' });
ok(player.el().style.width === '', 'Width is not forced');
ok(player.el().style.height === '', 'Height is not forced');
ruleAST.declarations.forEach(function(dec){
rule[dec.property] = dec.value;
});
});
player.dispose();
return styleRules;
}
// Set only the width
player.width(100);
rules = getStyleRules();
equal(rules['.example_1-dimensions'].width, '100px', 'style width should equal the supplied width in pixels');
equal(rules['.example_1-dimensions'].height, '56.25px', 'style height should match the default aspect ratio of the width');
// Set the height
player.height(200);
rules = getStyleRules();
equal(rules['.example_1-dimensions'].height, '200px', 'style height should match the supplied height in pixels');
// Reset the width and height to defaults
player.width('');
player.height('');
rules = getStyleRules();
equal(rules['.example_1-dimensions'].width, '300px', 'supplying an empty string should reset the width');
equal(rules['.example_1-dimensions'].height, '168.75px', 'supplying an empty string should reset the height');
// Switch to fluid mode
player.fluid(true);
rules = getStyleRules();
ok(player.hasClass('vjs-fluid'), 'the vjs-fluid class should be added to the player');
equal(rules['.example_1-dimensions.vjs-fluid']['padding-top'], '56.25%', 'fluid aspect ratio should match the default aspect ratio');
// Change the aspect ratio
player.aspectRatio('4:1');
rules = getStyleRules();
equal(rules['.example_1-dimensions.vjs-fluid']['padding-top'], '25%', 'aspect ratio percent should match the newly set aspect ratio');
});
test('should wrap the original tag in the player div', function(){

0 comments on commit 2fc8968

Please sign in to comment.