diff options
Diffstat (limited to 'javascript/videojs/src/js/tech/setup-sourceset.js')
| -rw-r--r-- | javascript/videojs/src/js/tech/setup-sourceset.js | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/tech/setup-sourceset.js b/javascript/videojs/src/js/tech/setup-sourceset.js new file mode 100644 index 0000000..621b62f --- /dev/null +++ b/javascript/videojs/src/js/tech/setup-sourceset.js @@ -0,0 +1,310 @@ +import window from 'global/window'; +import document from 'global/document'; +import {merge} from '../utils/obj'; +import {getAbsoluteURL} from '../utils/url'; + +/** @import Html5 from './html5' */ + +/** + * This function is used to fire a sourceset when there is something + * similar to `mediaEl.load()` being called. It will try to find the source via + * the `src` attribute and then the `<source>` elements. It will then fire `sourceset` + * with the source that was found or empty string if we cannot know. If it cannot + * find a source then `sourceset` will not be fired. + * + * @param {Html5} tech + * The tech object that sourceset was setup on + * + * @return {boolean} + * returns false if the sourceset was not fired and true otherwise. + */ +const sourcesetLoad = (tech) => { + const el = tech.el(); + + // if `el.src` is set, that source will be loaded. + if (el.hasAttribute('src')) { + tech.triggerSourceset(el.src); + return true; + } + + /** + * Since there isn't a src property on the media element, source elements will be used for + * implementing the source selection algorithm. This happens asynchronously and + * for most cases were there is more than one source we cannot tell what source will + * be loaded, without re-implementing the source selection algorithm. At this time we are not + * going to do that. There are three special cases that we do handle here though: + * + * 1. If there are no sources, do not fire `sourceset`. + * 2. If there is only one `<source>` with a `src` property/attribute that is our `src` + * 3. If there is more than one `<source>` but all of them have the same `src` url. + * That will be our src. + */ + const sources = tech.$$('source'); + const srcUrls = []; + let src = ''; + + // if there are no sources, do not fire sourceset + if (!sources.length) { + return false; + } + + // only count valid/non-duplicate source elements + for (let i = 0; i < sources.length; i++) { + const url = sources[i].src; + + if (url && srcUrls.indexOf(url) === -1) { + srcUrls.push(url); + } + } + + // there were no valid sources + if (!srcUrls.length) { + return false; + } + + // there is only one valid source element url + // use that + if (srcUrls.length === 1) { + src = srcUrls[0]; + } + + tech.triggerSourceset(src); + return true; +}; + +/** + * our implementation of an `innerHTML` descriptor for browsers + * that do not have one. + */ +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()); + + // 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; + } +}); + +/** + * Get a property descriptor given a list of priorities and the + * property to get. + */ +const getDescriptor = (priority, prop) => { + let descriptor = {}; + + for (let i = 0; i < priority.length; i++) { + descriptor = Object.getOwnPropertyDescriptor(priority[i], prop); + + if (descriptor && descriptor.set && descriptor.get) { + break; + } + } + + descriptor.enumerable = true; + descriptor.configurable = true; + + 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 + * causes a `sourceset` if the the media element is ready and has no source. + * This happens when: + * - The page has just loaded and the media element does not have a source. + * - The media element was emptied of all sources, then `load()` was called. + * + * It does this by patching the following functions/properties when they are supported: + * + * - `append()` - can be used to add a `<source>` element to the media element + * - `appendChild()` - can be used to add a `<source>` element to the media element + * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element + * - `innerHTML` - can be used to add a `<source>` element to the media element + * + * @param {Html5} tech + * The tech object that sourceset is being setup on. + */ +const firstSourceWatch = function(tech) { + const el = tech.el(); + + // make sure firstSourceWatch isn't setup twice. + if (el.resetSourceWatch_) { + return; + } + + const old = {}; + const innerDescriptor = getInnerHTMLDescriptor(tech); + const appendWrapper = (appendFn) => (...args) => { + const retval = appendFn.apply(el, args); + + sourcesetLoad(tech); + + return retval; + }; + + ['append', 'appendChild', 'insertAdjacentHTML'].forEach((k) => { + if (!el[k]) { + return; + } + + // store the old function + old[k] = el[k]; + + // call the old function with a sourceset if a source + // was loaded + el[k] = appendWrapper(old[k]); + }); + + Object.defineProperty(el, 'innerHTML', merge(innerDescriptor, { + set: appendWrapper(innerDescriptor.set) + })); + + 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: + * + * - `src` - to determine when `src` is set + * - `setAttribute()` - to determine when `src` is set + * - `load()` - this re-triggers the source selection algorithm, and can + * cause a sourceset. + * + * If there is no source when we are adding `sourceset` support or during a `load()` + * we also patch the functions listed in `firstSourceWatch`. + * + * @param {Html5} tech + * The tech to patch + */ +const setupSourceset = function(tech) { + if (!tech.featuresSourceset) { + return; + } + + const el = tech.el(); + + // make sure sourceset isn't setup twice. + if (el.resetSourceset_) { + return; + } + + const srcDescriptor = getSrcDescriptor(tech); + const oldSetAttribute = el.setAttribute; + const oldLoad = el.load; + + Object.defineProperty(el, 'src', merge(srcDescriptor, { + set: (v) => { + const retval = srcDescriptor.set.call(el, v); + + // we use the getter here to get the actual value set on src + tech.triggerSourceset(el.src); + + return retval; + } + })); + + el.setAttribute = (n, v) => { + const retval = oldSetAttribute.call(el, n, v); + + if ((/src/i).test(n)) { + tech.triggerSourceset(el.src); + } + + return retval; + }; + + el.load = () => { + const retval = oldLoad.call(el); + + // if load was called, but there was no source to fire + // sourceset on. We have to watch for a source append + // 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; |
