- 1 :
import window from 'global/window';
- 2 :
import document from 'global/document';
- 3 :
import {merge} from '../utils/obj';
- 4 :
import {getAbsoluteURL} from '../utils/url';
- 5 :
- 6 :
/**
- 7 :
* @typedef { import('./html5').default } Html5
- 8 :
*/
- 9 :
- 10 :
/**
- 11 :
* This function is used to fire a sourceset when there is something
- 12 :
* similar to `mediaEl.load()` being called. It will try to find the source via
- 13 :
* the `src` attribute and then the `<source>` elements. It will then fire `sourceset`
- 14 :
* with the source that was found or empty string if we cannot know. If it cannot
- 15 :
* find a source then `sourceset` will not be fired.
- 16 :
*
- 17 :
* @param {Html5} tech
- 18 :
* The tech object that sourceset was setup on
- 19 :
*
- 20 :
* @return {boolean}
- 21 :
* returns false if the sourceset was not fired and true otherwise.
- 22 :
*/
- 23 :
const sourcesetLoad = (tech) => {
- 24 :
const el = tech.el();
- 25 :
- 26 :
// if `el.src` is set, that source will be loaded.
- 27 :
if (el.hasAttribute('src')) {
- 28 :
tech.triggerSourceset(el.src);
- 29 :
return true;
- 30 :
}
- 31 :
- 32 :
/**
- 33 :
* Since there isn't a src property on the media element, source elements will be used for
- 34 :
* implementing the source selection algorithm. This happens asynchronously and
- 35 :
* for most cases were there is more than one source we cannot tell what source will
- 36 :
* be loaded, without re-implementing the source selection algorithm. At this time we are not
- 37 :
* going to do that. There are three special cases that we do handle here though:
- 38 :
*
- 39 :
* 1. If there are no sources, do not fire `sourceset`.
- 40 :
* 2. If there is only one `<source>` with a `src` property/attribute that is our `src`
- 41 :
* 3. If there is more than one `<source>` but all of them have the same `src` url.
- 42 :
* That will be our src.
- 43 :
*/
- 44 :
const sources = tech.$$('source');
- 45 :
const srcUrls = [];
- 46 :
let src = '';
- 47 :
- 48 :
// if there are no sources, do not fire sourceset
- 49 :
if (!sources.length) {
- 50 :
return false;
- 51 :
}
- 52 :
- 53 :
// only count valid/non-duplicate source elements
- 54 :
for (let i = 0; i < sources.length; i++) {
- 55 :
const url = sources[i].src;
- 56 :
- 57 :
if (url && srcUrls.indexOf(url) === -1) {
- 58 :
srcUrls.push(url);
- 59 :
}
- 60 :
}
- 61 :
- 62 :
// there were no valid sources
- 63 :
if (!srcUrls.length) {
- 64 :
return false;
- 65 :
}
- 66 :
- 67 :
// there is only one valid source element url
- 68 :
// use that
- 69 :
if (srcUrls.length === 1) {
- 70 :
src = srcUrls[0];
- 71 :
}
- 72 :
- 73 :
tech.triggerSourceset(src);
- 74 :
return true;
- 75 :
};
- 76 :
- 77 :
/**
- 78 :
* our implementation of an `innerHTML` descriptor for browsers
- 79 :
* that do not have one.
- 80 :
*/
- 81 :
const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
- 82 :
get() {
- 83 :
return this.cloneNode(true).innerHTML;
- 84 :
},
- 85 :
set(v) {
- 86 :
// make a dummy node to use innerHTML on
- 87 :
const dummy = document.createElement(this.nodeName.toLowerCase());
- 88 :
- 89 :
// set innerHTML to the value provided
- 90 :
dummy.innerHTML = v;
- 91 :
- 92 :
// make a document fragment to hold the nodes from dummy
- 93 :
const docFrag = document.createDocumentFragment();
- 94 :
- 95 :
// copy all of the nodes created by the innerHTML on dummy
- 96 :
// to the document fragment
- 97 :
while (dummy.childNodes.length) {
- 98 :
docFrag.appendChild(dummy.childNodes[0]);
- 99 :
}
- 100 :
- 101 :
// remove content
- 102 :
this.innerText = '';
- 103 :
- 104 :
// now we add all of that html in one by appending the
- 105 :
// document fragment. This is how innerHTML does it.
- 106 :
window.Element.prototype.appendChild.call(this, docFrag);
- 107 :
- 108 :
// then return the result that innerHTML's setter would
- 109 :
return this.innerHTML;
- 110 :
}
- 111 :
});
- 112 :
- 113 :
/**
- 114 :
* Get a property descriptor given a list of priorities and the
- 115 :
* property to get.
- 116 :
*/
- 117 :
const getDescriptor = (priority, prop) => {
- 118 :
let descriptor = {};
- 119 :
- 120 :
for (let i = 0; i < priority.length; i++) {
- 121 :
descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
- 122 :
- 123 :
if (descriptor && descriptor.set && descriptor.get) {
- 124 :
break;
- 125 :
}
- 126 :
}
- 127 :
- 128 :
descriptor.enumerable = true;
- 129 :
descriptor.configurable = true;
- 130 :
- 131 :
return descriptor;
- 132 :
};
- 133 :
- 134 :
const getInnerHTMLDescriptor = (tech) => getDescriptor([
- 135 :
tech.el(),
- 136 :
window.HTMLMediaElement.prototype,
- 137 :
window.Element.prototype,
- 138 :
innerHTMLDescriptorPolyfill
- 139 :
], 'innerHTML');
- 140 :
- 141 :
/**
- 142 :
* Patches browser internal functions so that we can tell synchronously
- 143 :
* if a `<source>` was appended to the media element. For some reason this
- 144 :
* causes a `sourceset` if the the media element is ready and has no source.
- 145 :
* This happens when:
- 146 :
* - The page has just loaded and the media element does not have a source.
- 147 :
* - The media element was emptied of all sources, then `load()` was called.
- 148 :
*
- 149 :
* It does this by patching the following functions/properties when they are supported:
- 150 :
*
- 151 :
* - `append()` - can be used to add a `<source>` element to the media element
- 152 :
* - `appendChild()` - can be used to add a `<source>` element to the media element
- 153 :
* - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element
- 154 :
* - `innerHTML` - can be used to add a `<source>` element to the media element
- 155 :
*
- 156 :
* @param {Html5} tech
- 157 :
* The tech object that sourceset is being setup on.
- 158 :
*/
- 159 :
const firstSourceWatch = function(tech) {
- 160 :
const el = tech.el();
- 161 :
- 162 :
// make sure firstSourceWatch isn't setup twice.
- 163 :
if (el.resetSourceWatch_) {
- 164 :
return;
- 165 :
}
- 166 :
- 167 :
const old = {};
- 168 :
const innerDescriptor = getInnerHTMLDescriptor(tech);
- 169 :
const appendWrapper = (appendFn) => (...args) => {
- 170 :
const retval = appendFn.apply(el, args);
- 171 :
- 172 :
sourcesetLoad(tech);
- 173 :
- 174 :
return retval;
- 175 :
};
- 176 :
- 177 :
['append', 'appendChild', 'insertAdjacentHTML'].forEach((k) => {
- 178 :
if (!el[k]) {
- 179 :
return;
- 180 :
}
- 181 :
- 182 :
// store the old function
- 183 :
old[k] = el[k];
- 184 :
- 185 :
// call the old function with a sourceset if a source
- 186 :
// was loaded
- 187 :
el[k] = appendWrapper(old[k]);
- 188 :
});
- 189 :
- 190 :
Object.defineProperty(el, 'innerHTML', merge(innerDescriptor, {
- 191 :
set: appendWrapper(innerDescriptor.set)
- 192 :
}));
- 193 :
- 194 :
el.resetSourceWatch_ = () => {
- 195 :
el.resetSourceWatch_ = null;
- 196 :
Object.keys(old).forEach((k) => {
- 197 :
el[k] = old[k];
- 198 :
});
- 199 :
- 200 :
Object.defineProperty(el, 'innerHTML', innerDescriptor);
- 201 :
};
- 202 :
- 203 :
// on the first sourceset, we need to revert our changes
- 204 :
tech.one('sourceset', el.resetSourceWatch_);
- 205 :
};
- 206 :
- 207 :
/**
- 208 :
* our implementation of a `src` descriptor for browsers
- 209 :
* that do not have one
- 210 :
*/
- 211 :
const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
- 212 :
get() {
- 213 :
if (this.hasAttribute('src')) {
- 214 :
return getAbsoluteURL(window.Element.prototype.getAttribute.call(this, 'src'));
- 215 :
}
- 216 :
- 217 :
return '';
- 218 :
},
- 219 :
set(v) {
- 220 :
window.Element.prototype.setAttribute.call(this, 'src', v);
- 221 :
- 222 :
return v;
- 223 :
}
- 224 :
});
- 225 :
- 226 :
const getSrcDescriptor = (tech) => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
- 227 :
- 228 :
/**
- 229 :
* setup `sourceset` handling on the `Html5` tech. This function
- 230 :
* patches the following element properties/functions:
- 231 :
*
- 232 :
* - `src` - to determine when `src` is set
- 233 :
* - `setAttribute()` - to determine when `src` is set
- 234 :
* - `load()` - this re-triggers the source selection algorithm, and can
- 235 :
* cause a sourceset.
- 236 :
*
- 237 :
* If there is no source when we are adding `sourceset` support or during a `load()`
- 238 :
* we also patch the functions listed in `firstSourceWatch`.
- 239 :
*
- 240 :
* @param {Html5} tech
- 241 :
* The tech to patch
- 242 :
*/
- 243 :
const setupSourceset = function(tech) {
- 244 :
if (!tech.featuresSourceset) {
- 245 :
return;
- 246 :
}
- 247 :
- 248 :
const el = tech.el();
- 249 :
- 250 :
// make sure sourceset isn't setup twice.
- 251 :
if (el.resetSourceset_) {
- 252 :
return;
- 253 :
}
- 254 :
- 255 :
const srcDescriptor = getSrcDescriptor(tech);
- 256 :
const oldSetAttribute = el.setAttribute;
- 257 :
const oldLoad = el.load;
- 258 :
- 259 :
Object.defineProperty(el, 'src', merge(srcDescriptor, {
- 260 :
set: (v) => {
- 261 :
const retval = srcDescriptor.set.call(el, v);
- 262 :
- 263 :
// we use the getter here to get the actual value set on src
- 264 :
tech.triggerSourceset(el.src);
- 265 :
- 266 :
return retval;
- 267 :
}
- 268 :
}));
- 269 :
- 270 :
el.setAttribute = (n, v) => {
- 271 :
const retval = oldSetAttribute.call(el, n, v);
- 272 :
- 273 :
if ((/src/i).test(n)) {
- 274 :
tech.triggerSourceset(el.src);
- 275 :
}
- 276 :
- 277 :
return retval;
- 278 :
};
- 279 :
- 280 :
el.load = () => {
- 281 :
const retval = oldLoad.call(el);
- 282 :
- 283 :
// if load was called, but there was no source to fire
- 284 :
// sourceset on. We have to watch for a source append
- 285 :
// as that can trigger a `sourceset` when the media element
- 286 :
// has no source
- 287 :
if (!sourcesetLoad(tech)) {
- 288 :
tech.triggerSourceset('');
- 289 :
firstSourceWatch(tech);
- 290 :
}
- 291 :
- 292 :
return retval;
- 293 :
};
- 294 :
- 295 :
if (el.currentSrc) {
- 296 :
tech.triggerSourceset(el.currentSrc);
- 297 :
} else if (!sourcesetLoad(tech)) {
- 298 :
firstSourceWatch(tech);
- 299 :
}
- 300 :
- 301 :
el.resetSourceset_ = () => {
- 302 :
el.resetSourceset_ = null;
- 303 :
el.load = oldLoad;
- 304 :
el.setAttribute = oldSetAttribute;
- 305 :
Object.defineProperty(el, 'src', srcDescriptor);
- 306 :
if (el.resetSourceWatch_) {
- 307 :
el.resetSourceWatch_();
- 308 :
}
- 309 :
};
- 310 :
};
- 311 :
- 312 :
export default setupSourceset;