1
0
mirror of https://github.com/videojs/video.js.git synced 2025-10-31 00:08:01 +02:00

fix: sourceset and browser behavior inconsistencies (#5054)

* We now trigger `sourceset` any time a `<source>` element is appended to a mediaEl with no source.
  * `load` should always fire a `sourceset`
  * `sourceset` should always be the absolute url.
This commit is contained in:
Brandon Casey
2018-05-09 15:51:47 -04:00
committed by GitHub
parent ba2ae7868b
commit 6147e5f7aa
5 changed files with 195 additions and 197 deletions

View File

@@ -2612,9 +2612,11 @@ class Player extends Component {
if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
this.changingSrc_ = true;
// load this technology with the chosen source
this.loadTech_(sourceTech.tech, sourceTech.source);
this.tech_.ready(() => {
this.changingSrc_ = false;
});
return false;
}

View File

@@ -35,10 +35,6 @@ class Html5 extends Tech {
constructor(options, ready) {
super(options, ready);
if (options.enableSourceset) {
this.setupSourcesetHandling_();
}
const source = options.source;
let crossoriginTracks = false;
@@ -52,6 +48,11 @@ class Html5 extends Tech {
this.handleLateInit_(this.el_);
}
// setup sourceset after late sourceset/init
if (options.enableSourceset) {
this.setupSourcesetHandling_();
}
if (this.el_.hasChildNodes()) {
const nodes = this.el_.childNodes;
@@ -117,6 +118,9 @@ class Html5 extends Tech {
* Dispose of `HTML5` media element and remove all tracks.
*/
dispose() {
if (this.el_.resetSourceset_) {
this.el_.resetSourceset_();
}
Html5.disposeMediaElement(this.el_);
this.options_ = null;

View File

@@ -1,6 +1,7 @@
import window from 'global/window';
import document from 'global/document';
import mergeOptions from '../utils/merge-options';
import {getAbsoluteURL} from '../utils/url';
/**
* This function is used to fire a sourceset when there is something
@@ -19,7 +20,7 @@ const sourcesetLoad = (tech) => {
const el = tech.el();
// if `el.src` is set, that source will be loaded.
if (el.src) {
if (el.hasAttribute('src')) {
tech.triggerSourceset(el.src);
return true;
}
@@ -56,7 +57,7 @@ const sourcesetLoad = (tech) => {
// there were no valid sources
if (!srcUrls.length) {
return;
return false;
}
// there is only one valid source element url
@@ -70,114 +71,69 @@ const sourcesetLoad = (tech) => {
};
/**
* Get the browsers property descriptor for the `innerHTML`
* property. This will allow us to overwrite it without
* destroying native functionality.
*
* @param {HTMLMediaElement} el
* The tech element that should be used to get the descriptor
*
* @return {Object}
* The property descriptor for innerHTML.
* our implementation of an `innerHTML` descriptor for browsers
* that do not have one.
*/
const getInnerHTMLDescriptor = (el) => {
const proto = window.Element.prototype;
let innerDescriptor = {};
const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
get() {
return this.cloneNode(true).innerHTML;
},
set(v) {
// make a dummy node to use innerHTML on
const dummy = document.createElement(this.nodeName.toLowerCase());
// preserve getters/setters already on `el.innerHTML` if they exist
if (Object.getOwnPropertyDescriptor(el, 'innerHTML')) {
innerDescriptor = Object.getOwnPropertyDescriptor(el, 'innerHTML');
} else if (Object.getOwnPropertyDescriptor(proto, 'innerHTML')) {
innerDescriptor = Object.getOwnPropertyDescriptor(proto, 'innerHTML');
// set innerHTML to the value provided
dummy.innerHTML = v;
// make a document fragment to hold the nodes from dummy
const docFrag = document.createDocumentFragment();
// copy all of the nodes created by the innerHTML on dummy
// to the document fragment
while (dummy.childNodes.length) {
docFrag.appendChild(dummy.childNodes[0]);
}
// remove content
this.innerText = '';
// now we add all of that html in one by appending the
// document fragment. This is how innerHTML does it.
window.Element.prototype.appendChild.call(this, docFrag);
// then return the result that innerHTML's setter would
return this.innerHTML;
}
if (!innerDescriptor.get) {
innerDescriptor.get = function() {
return el.cloneNode().innerHTML;
};
}
if (!innerDescriptor.set) {
innerDescriptor.set = function(v) {
// remove all current content from inside
el.innerText = '';
// make a dummy node to use innerHTML on
const dummy = document.createElement(el.nodeName.toLowerCase());
// set innerHTML to the value provided
dummy.innerHTML = v;
// make a document fragment to hold the nodes from dummy
const docFrag = document.createDocumentFragment();
// copy all of the nodes created by the innerHTML on dummy
// to the document fragment
while (dummy.childNodes.length) {
docFrag.appendChild(dummy.childNodes[0]);
}
// now we add all of that html in one by appending the
// document fragment. This is how innerHTML does it.
window.Element.prototype.appendChild.call(el, docFrag);
// then return the result that innerHTML's setter would
return el.innerHTML;
};
}
if (typeof innerDescriptor.enumerable === 'undefined') {
innerDescriptor.enumerable = true;
}
innerDescriptor.configurable = true;
return innerDescriptor;
};
});
/**
* Get the browsers property descriptor for the `src`
* property. This will allow us to overwrite it without
* destroying native functionality.
*
* @param {HTMLMediaElement} el
* The tech element that should be used to get the descriptor
*
* @return {Object}
* The property descriptor for `src`.
* Get a property descriptor given a list of priorities and the
* property to get.
*/
const getSrcDescriptor = (el) => {
const proto = window.HTMLMediaElement.prototype;
let srcDescriptor = {};
const getDescriptor = (priority, prop) => {
let descriptor = {};
// preserve getters/setters already on `el.src` if they exist
if (Object.getOwnPropertyDescriptor(el, 'src')) {
srcDescriptor = Object.getOwnPropertyDescriptor(el, 'src');
} else if (Object.getOwnPropertyDescriptor(proto, 'src')) {
srcDescriptor = mergeOptions(srcDescriptor, Object.getOwnPropertyDescriptor(proto, 'src'));
for (let i = 0; i < priority.length; i++) {
descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
if (descriptor && descriptor.set && descriptor.get) {
break;
}
}
if (!srcDescriptor.get) {
srcDescriptor.get = function() {
return proto.getAttribute.call(el, 'src');
};
}
descriptor.enumerable = true;
descriptor.configurable = true;
if (!srcDescriptor.set) {
srcDescriptor.set = function(v) {
return proto.setAttribute.call(el, 'src', v);
};
}
if (typeof srcDescriptor.enumerable === 'undefined') {
srcDescriptor.enumerable = true;
}
srcDescriptor.configurable = true;
return srcDescriptor;
return descriptor;
};
const getInnerHTMLDescriptor = (tech) => getDescriptor([
tech.el(),
window.HTMLMediaElement.prototype,
window.Element.prototype,
innerHTMLDescriptorPolyfill
], 'innerHTML');
/**
* Patches browser internal functions so that we can tell synchronously
* if a `<source>` was appended to the media element. For some reason this
@@ -200,74 +156,71 @@ const firstSourceWatch = function(tech) {
const el = tech.el();
// make sure firstSourceWatch isn't setup twice.
if (el.firstSourceWatch_) {
if (el.resetSourceWatch_) {
return;
}
el.firstSourceWatch_ = true;
const oldAppend = el.append;
const oldAppendChild = el.appendChild;
const oldInsertAdjacentHTML = el.insertAdjacentHTML;
const innerDescriptor = getInnerHTMLDescriptor(el);
el.appendChild = function() {
const retval = oldAppendChild.apply(el, arguments);
const old = {};
const innerDescriptor = getInnerHTMLDescriptor(tech);
const appendWrapper = (appendFn) => (...args) => {
const retval = appendFn.apply(el, args);
sourcesetLoad(tech);
return retval;
};
if (oldAppend) {
el.append = function() {
const retval = oldAppend.apply(el, arguments);
['append', 'appendChild', 'insertAdjacentHTML'].forEach((k) => {
if (!el[k]) {
return;
}
sourcesetLoad(tech);
// store the old function
old[k] = el[k];
return retval;
};
}
if (oldInsertAdjacentHTML) {
el.insertAdjacentHTML = function() {
const retval = oldInsertAdjacentHTML.apply(el, arguments);
sourcesetLoad(tech);
return retval;
};
}
Object.defineProperty(el, 'innerHTML', {
get: innerDescriptor.get.bind(el),
set(v) {
const retval = innerDescriptor.set.call(el, v);
sourcesetLoad(tech);
return retval;
},
configurable: true,
enumerable: innerDescriptor.enumerable
// call the old function with a sourceset if a source
// was loaded
el[k] = appendWrapper(old[k]);
});
// on the first sourceset, we need to revert
// our changes
tech.one('sourceset', (e) => {
el.firstSourceWatch_ = false;
el.appendChild = oldAppendChild;
Object.defineProperty(el, 'innerHTML', mergeOptions(innerDescriptor, {
set: appendWrapper(innerDescriptor.set)
}));
if (oldAppend) {
el.append = oldAppend;
}
if (oldInsertAdjacentHTML) {
el.insertAdjacentHTML = oldInsertAdjacentHTML;
}
el.resetSourceWatch_ = () => {
el.resetSourceWatch_ = null;
Object.keys(old).forEach((k) => {
el[k] = old[k];
});
Object.defineProperty(el, 'innerHTML', innerDescriptor);
});
};
// on the first sourceset, we need to revert our changes
tech.one('sourceset', el.resetSourceWatch_);
};
/**
* our implementation of a `src` descriptor for browsers
* that do not have one.
*/
const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
get() {
if (this.hasAttribute('src')) {
return getAbsoluteURL(window.Element.prototype.getAttribute.call(this, 'src'));
}
return '';
},
set(v) {
window.Element.prototype.setAttribute.call(this, 'src', v);
return v;
}
});
const getSrcDescriptor = (tech) => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
/**
* setup `sourceset` handling on the `Html5` tech. This function
* patches the following element properties/functions:
@@ -291,35 +244,15 @@ const setupSourceset = function(tech) {
const el = tech.el();
// make sure sourceset isn't setup twice.
if (el.setupSourceset_) {
if (el.resetSourceset_) {
return;
}
el.setupSourceset_ = true;
const srcDescriptor = getSrcDescriptor(el);
const srcDescriptor = getSrcDescriptor(tech);
const oldSetAttribute = el.setAttribute;
const oldLoad = el.load;
// we need to fire sourceset when the player is ready
// if we find that the media element had a src when it was
// given to us and that tech element is not in a stalled state
if (el.src || el.currentSrc && el.initNetworkState_ !== 3) {
if (el.currentSrc) {
tech.triggerSourceset(el.currentSrc);
} else {
sourcesetLoad(tech);
}
}
// for some reason adding a source element when a mediaElement has no source
// calls `load` internally right away. We need to handle that.
if (!el.src && !el.currentSrc && !tech.$$('source').length) {
firstSourceWatch(tech);
}
Object.defineProperty(el, 'src', {
get: srcDescriptor.get.bind(el),
Object.defineProperty(el, 'src', mergeOptions(srcDescriptor, {
set: (v) => {
const retval = srcDescriptor.set.call(el, v);
@@ -327,16 +260,14 @@ const setupSourceset = function(tech) {
tech.triggerSourceset(el.src);
return retval;
},
configurable: true,
enumerable: srcDescriptor.enumerable
});
}
}));
el.setAttribute = (n, v) => {
const retval = oldSetAttribute.call(el, n, v);
if (n === 'src') {
tech.triggerSourceset(el.getAttribute('src'));
if ((/src/i).test(n)) {
tech.triggerSourceset(el.src);
}
return retval;
@@ -350,11 +281,28 @@ const setupSourceset = function(tech) {
// as that can trigger a `sourceset` when the media element
// has no source
if (!sourcesetLoad(tech)) {
tech.triggerSourceset('');
firstSourceWatch(tech);
}
return retval;
};
if (el.currentSrc) {
tech.triggerSourceset(el.currentSrc);
} else if (!sourcesetLoad(tech)) {
firstSourceWatch(tech);
}
el.resetSourceset_ = () => {
el.resetSourceset_ = null;
el.load = oldLoad;
el.setAttribute = oldSetAttribute;
Object.defineProperty(el, 'src', srcDescriptor);
if (el.resetSourceWatch_) {
el.resetSourceWatch_();
}
};
};
export default setupSourceset;

View File

@@ -35,10 +35,10 @@ const filterSource = function(src) {
src = newsrc;
} else if (typeof src === 'string' && src.trim()) {
// convert string into object
src = [checkMimetype({src})];
src = [fixSource({src})];
} else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
// src is already valid
src = [checkMimetype(src)];
src = [fixSource(src)];
} else {
// invalid source, turn it into an empty array
src = [];
@@ -55,7 +55,7 @@ const filterSource = function(src) {
* @return {Tech~SourceObject}
* src Object with known type
*/
function checkMimetype(src) {
function fixSource(src) {
const mimetype = getMimetype(src.src);
if (!src.type && mimetype) {

View File

@@ -68,7 +68,9 @@ const setupEnv = function(env, testName) {
}
env.sourcesets = 0;
env.hook = (player) => player.on('sourceset', () => env.sourcesets++);
env.hook = (player) => player.on('sourceset', (e) => {
env.sourcesets++;
});
videojs.hook('setup', env.hook);
if ((/audio/i).test(testName)) {
@@ -269,6 +271,51 @@ QUnit[qunitFn]('sourceset', function(hooks) {
done();
}, wait);
});
QUnit.test('relative sources are handled correctly', function(assert) {
const done = assert.async();
const one = {src: 'relative-one.mp4', type: 'video/mp4'};
const two = {src: '../relative-two.mp4', type: 'video/mp4'};
const three = {src: './relative-three.mp4?test=test', type: 'video/mp4'};
const source = document.createElement('source');
source.src = one.src;
source.type = one.type;
this.mediaEl.appendChild(source);
this.player = videojs(this.mediaEl, {enableSourceset: true});
// mediaEl changes on ready
this.player.ready(() => {
this.mediaEl = this.player.tech_.el();
});
this.totalSourcesets = 3;
this.player.one('sourceset', (e) => {
assert.ok(true, '** sourceset with relative source and <source> el');
// mediaEl attr is relative
validateSource(this.player, {src: getAbsoluteURL(one.src), type: one.type}, e, {attr: one.src});
this.player.one('sourceset', (e2) => {
assert.ok(true, '** sourceset with relative source and mediaEl.src');
// mediaEl attr is relative
validateSource(this.player, {src: getAbsoluteURL(two.src), type: two.type}, e2, {attr: two.src});
// setAttribute makes the source absolute
this.player.one('sourceset', (e3) => {
assert.ok(true, '** sourceset with relative source and mediaEl.setAttribute');
validateSource(this.player, {src: getAbsoluteURL(three.src), type: three.type}, e3, {attr: three.src});
done();
});
this.mediaEl.setAttribute('src', three.src);
});
this.mediaEl.src = two.src;
});
});
}));
QUnit.module('source after player', (subhooks) => testTypes.forEach((testName) => {
@@ -540,7 +587,7 @@ QUnit[qunitFn]('sourceset', function(hooks) {
QUnit.test(`set, remove, load, and set again through ${appendObj.name}`, function(assert) {
const done = assert.async();
this.totalSourcesets = 2;
this.totalSourcesets = 3;
this.source = document.createElement('source');
this.source.src = sourceTwo.src;
this.source.type = sourceTwo.type;
@@ -551,8 +598,12 @@ QUnit[qunitFn]('sourceset', function(hooks) {
validateSource(this.player, [sourceOne], e1);
this.player.one('sourceset', (e2) => {
validateSource(this.player, sourceTwo, e2, {prop: '', attr: ''});
done();
validateSource(this.player, [{src: '', type: ''}], e2);
this.player.one('sourceset', (e3) => {
validateSource(this.player, sourceTwo, e3, {prop: '', attr: ''});
done();
});
});
// reset to no source
@@ -570,17 +621,10 @@ QUnit[qunitFn]('sourceset', function(hooks) {
});
QUnit.test('no source and load', function(assert) {
const done = assert.async();
this.player = videojs(this.mediaEl, {enableSourceset: true});
this.player.tech_.el_.load();
this.totalSourcesets = 0;
window.setTimeout(() => {
assert.equal(this.sourcesets, 0, 'no sourceset');
done();
}, wait);
this.totalSourcesets = 1;
});
}));