1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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;
|