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; +});