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