mirror of
https://github.com/videojs/video.js.git
synced 2024-12-25 02:42:10 +02:00
@heff added support for fluid widths, aspect ratios, and metadata defaults. closes #1952
This commit is contained in:
parent
6128305cc3
commit
2fc8968002
@ -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,7 +1,15 @@
|
||||
.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;
|
||||
@ -9,8 +17,6 @@
|
||||
/* 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!
|
||||
|
||||
|
149
src/js/player.js
149
src/js/player.js
@ -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,6 +2053,14 @@ 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'); },
|
||||
@ -1929,8 +2068,6 @@ class Player extends Component {
|
||||
// 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(){
|
||||
|
Loading…
Reference in New Issue
Block a user