From 74530d8b3afe71a28ffc36a2623bd938a9d419ad Mon Sep 17 00:00:00 2001 From: Gary Katsevman Date: Mon, 19 Dec 2016 11:51:42 -0500 Subject: [PATCH] feat(player): ingest a player div for videojs (#3856) If the videojs embed code (a video element) is wrapped in a div with the 'data-vjs-player' attribute on it, that element will be used for the player div and a new one will not be created. In addition, on browsers like iOS that don't support moving the media element inside the DOM, we will not need to clone the element and we could continue to re-use the same video element give to us in the embed code. This could also be extended in the future to change our embed code to a div-only approach if we so choose. --- src/js/player.js | 14 ++++-- src/js/tech/html5.js | 10 +++- test/unit/player.test.js | 87 +++++++++++++++++++++++++++++++++ test/unit/video.test.js | 102 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 5 deletions(-) diff --git a/src/js/player.js b/src/js/player.js index a943e193fe..57355fc250 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -490,8 +490,15 @@ class Player extends Component { * The DOM element that gets created. */ createEl() { - const el = this.el_ = super.createEl('div'); const tag = this.tag; + let el; + const playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute('data-vjs-player'); + + if (playerElIngest) { + el = this.el_ = tag.parentNode; + } else { + el = this.el_ = super.createEl('div'); + } // Remove width/height attrs from tag so CSS can make it 100% width/height tag.removeAttribute('width'); @@ -505,7 +512,7 @@ class Player extends Component { // workaround so we don't totally break IE7 // http://stackoverflow.com/questions/3653444/css-styles-not-applied-on-dynamic-elements-in-internet-explorer-7 if (attr === 'class') { - el.className = attrs[attr]; + el.className += ' ' + attrs[attr]; } else { el.setAttribute(attr, attrs[attr]); } @@ -555,7 +562,7 @@ class Player extends Component { tag.initNetworkState_ = tag.networkState; // Wrap video tag in div (el/box) container - if (tag.parentNode) { + if (tag.parentNode && !playerElIngest) { tag.parentNode.insertBefore(el, tag); } @@ -836,6 +843,7 @@ class Player extends Component { 'muted': this.options_.muted, 'poster': this.poster(), 'language': this.language(), + 'playerElIngest': this.playerElIngest_ || false, 'vtt.js': this.options_['vtt.js'] }, this.options_[techName.toLowerCase()]); diff --git a/src/js/tech/html5.js b/src/js/tech/html5.js index 32037828eb..2f739bb0bc 100644 --- a/src/js/tech/html5.js +++ b/src/js/tech/html5.js @@ -185,15 +185,21 @@ class Html5 extends Tech { // Check if this browser supports moving the element into the box. // On the iPhone video will break if you move the element, // So we have to create a brand new element. - if (!el || this.movingMediaElementInDOM === false) { + // If we ingested the player div, we do not need to move the media element. + if (!el || + !(this.options_.playerElIngest || + this.movingMediaElementInDOM)) { // If the original tag is still there, clone and remove it. if (el) { const clone = el.cloneNode(true); - el.parentNode.insertBefore(clone, el); + if (el.parentNode) { + el.parentNode.insertBefore(clone, el); + } Html5.disposeMediaElement(el); el = clone; + } else { el = document.createElement('video'); diff --git a/test/unit/player.test.js b/test/unit/player.test.js index 8d325a67c2..c374776219 100644 --- a/test/unit/player.test.js +++ b/test/unit/player.test.js @@ -830,6 +830,93 @@ QUnit.test('should restore attributes from the original video tag when creating assert.equal(el.getAttribute('webkit-playsinline'), '', 'webkit-playsinline attribute was set properly'); }); +QUnit.test('if tag exists and movingMediaElementInDOM, re-use the tag', function(assert) { + // simulate attributes stored from the original tag + const tag = Dom.createEl('video'); + + tag.setAttribute('preload', 'auto'); + tag.setAttribute('autoplay', ''); + tag.setAttribute('webkit-playsinline', ''); + + const html5Mock = { + options_: { + tag, + playerElIngest: false + }, + movingMediaElementInDOM: true + }; + + // set options that should override tag attributes + html5Mock.options_.preload = 'none'; + + // create the element + const el = Html5.prototype.createEl.call(html5Mock); + + assert.equal(el.getAttribute('preload'), 'none', 'attribute was successful overridden by an option'); + assert.equal(el.getAttribute('autoplay'), '', 'autoplay attribute was set properly'); + assert.equal(el.getAttribute('webkit-playsinline'), '', 'webkit-playsinline attribute was set properly'); + + assert.equal(el, tag, 'we have re-used the tag as expected'); +}); + +QUnit.test('if tag exists and *not* movingMediaElementInDOM, create a new tag', function(assert) { + // simulate attributes stored from the original tag + const tag = Dom.createEl('video'); + + tag.setAttribute('preload', 'auto'); + tag.setAttribute('autoplay', ''); + tag.setAttribute('webkit-playsinline', ''); + + const html5Mock = { + options_: { + tag, + playerElIngest: false + }, + movingMediaElementInDOM: false + }; + + // set options that should override tag attributes + html5Mock.options_.preload = 'none'; + + // create the element + const el = Html5.prototype.createEl.call(html5Mock); + + assert.equal(el.getAttribute('preload'), 'none', 'attribute was successful overridden by an option'); + assert.equal(el.getAttribute('autoplay'), '', 'autoplay attribute was set properly'); + assert.equal(el.getAttribute('webkit-playsinline'), '', 'webkit-playsinline attribute was set properly'); + + assert.notEqual(el, tag, 'we have not re-used the tag as expected'); +}); + +QUnit.test('if tag exists and *not* movingMediaElementInDOM, but playerElIngest re-use tag', function(assert) { + // simulate attributes stored from the original tag + const tag = Dom.createEl('video'); + + tag.setAttribute('preload', 'auto'); + tag.setAttribute('autoplay', ''); + tag.setAttribute('webkit-playsinline', ''); + + const html5Mock = { + options_: { + tag, + playerElIngest: true + }, + movingMediaElementInDOM: false + }; + + // set options that should override tag attributes + html5Mock.options_.preload = 'none'; + + // create the element + const el = Html5.prototype.createEl.call(html5Mock); + + assert.equal(el.getAttribute('preload'), 'none', 'attribute was successful overridden by an option'); + assert.equal(el.getAttribute('autoplay'), '', 'autoplay attribute was set properly'); + assert.equal(el.getAttribute('webkit-playsinline'), '', 'webkit-playsinline attribute was set properly'); + + assert.equal(el, tag, 'we have re-used the tag as expected'); +}); + QUnit.test('should honor default inactivity timeout', function(assert) { const clock = sinon.useFakeTimers(); diff --git a/test/unit/video.test.js b/test/unit/video.test.js index 0ba89af735..c9252613c1 100644 --- a/test/unit/video.test.js +++ b/test/unit/video.test.js @@ -40,6 +40,9 @@ QUnit.test('should return a video player instance', function(assert) { const player2 = videojs(tag2, { techOrder: ['techFaker'] }); assert.ok(player2.id() === 'test_vid_id2', 'created player from element'); + + player.dispose(); + player2.dispose(); }); QUnit.test('should return a video player instance from el html5 tech', function(assert) { @@ -66,6 +69,9 @@ QUnit.test('should return a video player instance from el html5 tech', function( const player2 = videojs(tag2, { techOrder: ['techFaker'] }); assert.ok(player2.id() === 'test_vid_id2', 'created player from element'); + + player.dispose(); + player2.dispose(); }); QUnit.test('should return a video player instance from el techfaker', function(assert) { @@ -91,6 +97,9 @@ QUnit.test('should return a video player instance from el techfaker', function(a const player2 = videojs(tag2, { techOrder: ['techFaker'] }); assert.ok(player2.id() === 'test_vid_id2', 'created player from element'); + + player.dispose(); + player2.dispose(); }); QUnit.test('should add the value to the languages object', function(assert) { @@ -166,3 +175,96 @@ QUnit.test('should expose DOM functions', function(assert) { `videojs.${vjsName} is a reference to Dom.${domName}`); }); }); + +QUnit.test('ingest player div if data-vjs-player attribute is present on video parentNode', function(assert) { + const fixture = document.querySelector('#qunit-fixture'); + + fixture.innerHTML = ` +
+ +
+ `; + + const playerDiv = document.querySelector('.foo'); + const vid = document.querySelector('#test_vid_id'); + + const player = videojs(vid, { + techOrder: ['html5'] + }); + + assert.equal(player.el(), playerDiv, 'we re-used the given div'); + assert.ok(player.hasClass('foo'), 'keeps any classes that were around previously'); + + player.dispose(); +}); + +QUnit.test('ingested player div should not create a new tag for movingMediaElementInDOM', function(assert) { + const Html5 = videojs.getTech('Html5'); + const oldIS = Html5.isSupported; + const oldMoving = Html5.prototype.movingMediaElementInDOM; + const oldCPT = Html5.nativeSourceHandler.canPlayType; + const fixture = document.querySelector('#qunit-fixture'); + + fixture.innerHTML = ` +
+ +
+ `; + Html5.prototype.movingMediaElementInDOM = false; + Html5.isSupported = () => true; + Html5.nativeSourceHandler.canPlayType = () => true; + + const playerDiv = document.querySelector('.foo'); + const vid = document.querySelector('#test_vid_id'); + + const player = videojs(vid, { + techOrder: ['html5'] + }); + + assert.equal(player.el(), playerDiv, 'we re-used the given div'); + assert.equal(player.tech_.el(), vid, 'we re-used the video element'); + assert.ok(player.hasClass('foo'), 'keeps any classes that were around previously'); + + player.dispose(); + Html5.prototype.movingMediaElementInDOM = oldMoving; + Html5.isSupported = oldIS; + Html5.nativeSourceHandler.canPlayType = oldCPT; +}); + +QUnit.test('should create a new tag for movingMediaElementInDOM', function(assert) { + const Html5 = videojs.getTech('Html5'); + const oldMoving = Html5.prototype.movingMediaElementInDOM; + const oldCPT = Html5.nativeSourceHandler.canPlayType; + const fixture = document.querySelector('#qunit-fixture'); + const oldIS = Html5.isSupported; + + fixture.innerHTML = ` +
+ +
+ `; + Html5.prototype.movingMediaElementInDOM = false; + Html5.isSupported = () => true; + Html5.nativeSourceHandler.canPlayType = () => true; + + const playerDiv = document.querySelector('.foo'); + const vid = document.querySelector('#test_vid_id'); + + const player = videojs(vid, { + techOrder: ['html5'] + }); + + assert.notEqual(player.el(), playerDiv, 'we used a new div'); + assert.notEqual(player.tech_.el(), vid, 'we a new video element'); + + player.dispose(); + Html5.prototype.movingMediaElementInDOM = oldMoving; + Html5.isSupported = oldIS; + Html5.nativeSourceHandler.canPlayType = oldCPT; +});